diff options
588 files changed, 15221 insertions, 10164 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 8bfac03060b5..4e05cbc23dc3 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -110,6 +110,7 @@ aconfig_declarations_group { "com.android.window.flags.window-aconfig-java", "configinfra_framework_flags_java_exported_lib", "conscrypt_exported_aconfig_flags_lib", + "sdk_sandbox_exported_flags_lib", "device_policy_aconfig_flags_lib", "display_flags_lib", "dropbox_flags_lib", @@ -123,7 +124,6 @@ aconfig_declarations_group { "libcore_readonly_aconfig_flags_lib", "libgui_flags_java_lib", "power_flags_lib", - "sdk_sandbox_flags_lib", "surfaceflinger_flags_java_lib", "telecom_flags_core_java_lib", "telephony_flags_core_java_lib", diff --git a/Android.bp b/Android.bp index 303fa2cd18da..444725eb2c79 100644 --- a/Android.bp +++ b/Android.bp @@ -103,10 +103,10 @@ filegroup { ":android.hardware.gnss-V2-java-source", ":android.hardware.graphics.common-V3-java-source", ":android.hardware.keymaster-V4-java-source", - ":android.hardware.radio-V4-java-source", - ":android.hardware.radio.data-V4-java-source", - ":android.hardware.radio.network-V4-java-source", - ":android.hardware.radio.voice-V4-java-source", + ":android.hardware.radio-V5-java-source", + ":android.hardware.radio.data-V5-java-source", + ":android.hardware.radio.network-V5-java-source", + ":android.hardware.radio.voice-V5-java-source", ":android.hardware.security.secureclock-V1-java-source", ":android.hardware.thermal-V3-java-source", ":android.hardware.tv.tuner-V3-java-source", @@ -232,13 +232,13 @@ java_library { "android.hardware.gnss-V2.1-java", "android.hardware.health-V1.0-java-constants", "android.hardware.radio-V1.6-java", - "android.hardware.radio.data-V4-java", - "android.hardware.radio.ims-V3-java", - "android.hardware.radio.messaging-V4-java", - "android.hardware.radio.modem-V4-java", - "android.hardware.radio.network-V4-java", - "android.hardware.radio.sim-V4-java", - "android.hardware.radio.voice-V4-java", + "android.hardware.radio.data-V5-java", + "android.hardware.radio.ims-V4-java", + "android.hardware.radio.messaging-V5-java", + "android.hardware.radio.modem-V5-java", + "android.hardware.radio.network-V5-java", + "android.hardware.radio.sim-V5-java", + "android.hardware.radio.voice-V5-java", "android.hardware.thermal-V1.0-java-constants", "android.hardware.thermal-V1.0-java", "android.hardware.thermal-V1.1-java", diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java index abec170f3b7d..d52bbc245157 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java @@ -18,6 +18,7 @@ package com.android.server.job.controllers; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.annotation.NonNull; import android.app.job.JobInfo; import android.content.BroadcastReceiver; import android.content.Context; @@ -28,6 +29,7 @@ import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Log; @@ -56,7 +58,10 @@ public final class DeviceIdleJobsController extends StateController { private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); - private static final long BACKGROUND_JOBS_DELAY = 3000; + /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */ + private static final String DIJC_CONSTANT_PREFIX = "dijc_"; + private static final String KEY_BACKGROUND_JOBS_DELAY_MS = + DIJC_CONSTANT_PREFIX + "background_jobs_delay_ms"; static final int PROCESS_BACKGROUND_JOBS = 1; @@ -78,6 +83,8 @@ public final class DeviceIdleJobsController extends StateController { private int[] mDeviceIdleWhitelistAppIds; private int[] mPowerSaveTempWhitelistAppIds; + private long mBackgroundJobsDelay; + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -128,6 +135,9 @@ public final class DeviceIdleJobsController extends StateController { public DeviceIdleJobsController(JobSchedulerService service) { super(service); + mBackgroundJobsDelay = mContext.getResources().getInteger( + com.android.internal.R.integer.config_jobSchedulerBackgroundJobsDelay); + mHandler = new DeviceIdleJobsDelayHandler(AppSchedulingModuleThread.get().getLooper()); // Register for device idle mode changes mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); @@ -165,7 +175,7 @@ public final class DeviceIdleJobsController extends StateController { // When coming out of doze, process all foreground uids and EJs immediately, // while others will be processed after a delay of 3 seconds. mService.getJobStore().forEachJob(mShouldRushEvaluation, mDeviceIdleUpdateFunctor); - mHandler.sendEmptyMessageDelayed(PROCESS_BACKGROUND_JOBS, BACKGROUND_JOBS_DELAY); + mHandler.sendEmptyMessageDelayed(PROCESS_BACKGROUND_JOBS, mBackgroundJobsDelay); } } // Inform the job scheduler service about idle mode changes @@ -237,6 +247,26 @@ public final class DeviceIdleJobsController extends StateController { } @Override + public void processConstantLocked(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + switch (key) { + case KEY_BACKGROUND_JOBS_DELAY_MS: + mBackgroundJobsDelay = Math.max(0, properties.getLong(key, mBackgroundJobsDelay)); + break; + } + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + pw.println(); + pw.print(DeviceIdleJobsController.class.getSimpleName()); + pw.println(":"); + pw.increaseIndent(); + pw.print(KEY_BACKGROUND_JOBS_DELAY_MS, mBackgroundJobsDelay).println(); + pw.decreaseIndent(); + } + + @Override public void dumpControllerStateLocked(final IndentingPrintWriter pw, final Predicate<JobStatus> predicate) { pw.println("Idle mode: " + mDeviceIdleMode); diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp index 844e52c3ecf2..b0070c5faa36 100644 --- a/cmds/bootanimation/BootAnimation.cpp +++ b/cmds/bootanimation/BootAnimation.cpp @@ -207,7 +207,7 @@ BootAnimation::BootAnimation(sp<Callbacks> callbacks) : Thread(false), mLooper(new Looper(false)), mClockEnabled(true), mTimeIsAccurate(false), mTimeFormat12Hour(false), mTimeCheckThread(nullptr), mCallbacks(callbacks) { ATRACE_CALL(); - mSession = new SurfaceComposerClient(); + mSession = sp<SurfaceComposerClient>::make(); std::string powerCtl = android::base::GetProperty("sys.powerctl", ""); if (powerCtl.empty()) { diff --git a/core/api/current.txt b/core/api/current.txt index 3da5a5cca861..7bc0fb220e1a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -56108,6 +56108,17 @@ package android.view { method @NonNull public android.view.WindowInsets getWindowInsets(); } + @FlaggedApi("android.xr.xr_manifest_entries") public final class XrWindowProperties { + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_ACTIVITY_START_MODE = "android.window.PROPERTY_XR_ACTIVITY_START_MODE"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED = "android.window.PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED = "XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED = "XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_HOME_SPACE = "XR_ACTIVITY_START_MODE_HOME_SPACE"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_ACTIVITY_START_MODE_UNDEFINED = "XR_ACTIVITY_START_MODE_UNDEFINED"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_BOUNDARY_TYPE_LARGE = "XR_BOUNDARY_TYPE_LARGE"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_BOUNDARY_TYPE_NO_RECOMMENDATION = "XR_BOUNDARY_TYPE_NO_RECOMMENDATION"; + } + } package android.view.accessibility { diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 00ec48b79541..d651010b641a 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3031,6 +3031,7 @@ package android.provider { field public static final String DISABLED_PRINT_SERVICES = "disabled_print_services"; field @Deprecated public static final String ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES = "enabled_notification_policy_access_packages"; field public static final String ENABLED_VR_LISTENERS = "enabled_vr_listeners"; + field public static final String GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"; field public static final String IMMERSIVE_MODE_CONFIRMATIONS = "immersive_mode_confirmations"; field public static final String NOTIFICATION_BADGING = "notification_badging"; field public static final String NOTIFICATION_BUBBLES = "notification_bubbles"; diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index b4f653354e07..d5df48a2ea22 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -10060,9 +10060,11 @@ public class Activity extends ContextThemeWrapper } }); if (mJankTracker == null) { - // TODO b/377960907 use the Choreographer attached to the ViewRootImpl instead. - mJankTracker = new JankTracker(Choreographer.getInstance(), - decorView); + if (android.app.jank.Flags.viewrootChoreographer()) { + mJankTracker = new JankTracker(decorView); + } else { + mJankTracker = new JankTracker(Choreographer.getInstance(), decorView); + } } // TODO b/377674765 confirm this is the string we want logged. mJankTracker.setActivityName(getComponentName().getClassName()); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 4c9116b02c1d..2c1df73cc6cc 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -7362,16 +7362,6 @@ public final class ActivityThread extends ClientTransactionHandler } WindowManagerGlobal.getInstance().trimMemory(level); - - if (SystemProperties.getInt("debug.am.run_gc_trim_level", Integer.MAX_VALUE) <= level) { - unscheduleGcIdler(); - doGcIfNeeded("tm"); - } - if (SystemProperties.getInt("debug.am.run_mallopt_trim_level", Integer.MAX_VALUE) - <= level) { - unschedulePurgeIdler(); - purgePendingResources(); - } } private void setupGraphicsSupport(Context context) { diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java index 9c85b09f6be3..e3f67811757c 100644 --- a/core/java/android/app/jank/JankTracker.java +++ b/core/java/android/app/jank/JankTracker.java @@ -25,6 +25,7 @@ import android.view.AttachedSurfaceControl; import android.view.Choreographer; import android.view.SurfaceControl; import android.view.View; +import android.view.ViewRootImpl; import android.view.ViewTreeObserver; import com.android.internal.annotations.VisibleForTesting; @@ -100,7 +101,7 @@ public class JankTracker { public void run() { mDecorView.getViewTreeObserver() .removeOnWindowAttachListener(mOnWindowAttachListener); - registerForJankData(); + initializeJankTrackingComponents(); } }, REGISTRATION_DELAY_MS); } @@ -115,6 +116,7 @@ public class JankTracker { } }; + // TODO remove this once the viewroot_choreographer bugfix has been rolled out. b/399724640 public JankTracker(Choreographer choreographer, View decorView) { mStateTracker = new StateTracker(choreographer); mJankDataProcessor = new JankDataProcessor(mStateTracker); @@ -124,6 +126,19 @@ public class JankTracker { } /** + * Using this constructor delays the instantiation of the StateTracker and JankDataProcessor + * until after the OnWindowAttachListener is fired and the instance of Choreographer attached to + * the ViewRootImpl can be passed to StateTracker. This should ensures the vsync ids we are + * using to keep track of active states line up with the ids that are being returned by + * OnJankDataListener. + */ + public JankTracker(View decorView) { + mDecorView = decorView; + mHandlerThread.start(); + registerWindowListeners(); + } + + /** * Merges app jank stats reported by components outside the platform to the current pending * stats */ @@ -131,6 +146,9 @@ public class JankTracker { getHandler().post(new Runnable() { @Override public void run() { + if (mJankDataProcessor == null) { + return; + } mJankDataProcessor.mergeJankStats(appJankStats, mActivityName); } }); @@ -192,8 +210,7 @@ public class JankTracker { public void enableAppJankTracking() { // Add the activity as a state, this will ensure we track frames to the activity without the // need for a decorated widget to be used. - // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged. - mStateTracker.putState("NONE", mActivityName, "NONE"); + addActivityToStateTracking(); mTrackingEnabled = true; registerForJankData(); } @@ -203,27 +220,33 @@ public class JankTracker { */ public void disableAppJankTracking() { mTrackingEnabled = false; - // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged. - mStateTracker.removeState("NONE", mActivityName, "NONE"); + removeActivityFromStateTracking(); unregisterForJankData(); } /** - * Retrieve all pending widget states, this is intended for testing purposes only. + * Retrieve all pending widget states, this is intended for testing purposes only. If + * this is called before StateTracker has been created the method will just return without + * copying any data to the stateDataList parameter. * * @param stateDataList the ArrayList that will be populated with the pending states. */ @VisibleForTesting public void getAllUiStates(@NonNull ArrayList<StateTracker.StateData> stateDataList) { + if (mStateTracker == null) return; mStateTracker.retrieveAllStates(stateDataList); } /** * Retrieve all pending jank stats before they are logged, this is intended for testing - * purposes only. + * purposes only. If this method is called before JankDataProcessor is created it will return + * an empty HashMap. */ @VisibleForTesting public HashMap<String, JankDataProcessor.PendingJankStat> getPendingJankStats() { + if (mJankDataProcessor == null) { + return new HashMap<>(); + } return mJankDataProcessor.getPendingJankStats(); } @@ -233,8 +256,10 @@ public class JankTracker { */ @VisibleForTesting public void forceListenerRegistration() { + addActivityToStateTracking(); mSurfaceControl = mDecorView.getRootSurfaceControl(); registerJankDataListener(); + mListenersRegistered = true; } private void unregisterForJankData() { @@ -270,6 +295,10 @@ public class JankTracker { */ @VisibleForTesting public boolean shouldTrack() { + if (DEBUG) { + Log.d(DEBUG_KEY, String.format("mTrackingEnabled: %s | mListenersRegistered: %s", + mTrackingEnabled, mListenersRegistered)); + } return mTrackingEnabled && mListenersRegistered; } @@ -313,4 +342,36 @@ public class JankTracker { } return mHandler; } + + private void addActivityToStateTracking() { + if (mStateTracker == null) return; + + mStateTracker.putState(AppJankStats.WIDGET_CATEGORY_UNSPECIFIED, mActivityName, + AppJankStats.WIDGET_STATE_UNSPECIFIED); + } + + private void removeActivityFromStateTracking() { + if (mStateTracker == null) return; + + mStateTracker.removeState(AppJankStats.WIDGET_CATEGORY_UNSPECIFIED, mActivityName, + AppJankStats.WIDGET_STATE_UNSPECIFIED); + } + + private void initializeJankTrackingComponents() { + ViewRootImpl viewRoot = mDecorView.getViewRootImpl(); + if (viewRoot == null || viewRoot.getChoreographer() == null) { + return; + } + + if (mStateTracker == null) { + mStateTracker = new StateTracker(viewRoot.getChoreographer()); + } + + if (mJankDataProcessor == null) { + mJankDataProcessor = new JankDataProcessor(mStateTracker); + } + + addActivityToStateTracking(); + registerForJankData(); + } } diff --git a/core/java/android/app/jank/flags.aconfig b/core/java/android/app/jank/flags.aconfig index a62df1b3cbf7..de98b88d2aca 100644 --- a/core/java/android/app/jank/flags.aconfig +++ b/core/java/android/app/jank/flags.aconfig @@ -14,4 +14,14 @@ flag { namespace: "system_performance" description: "Controls whether the system will log frame metrics related to app jank" bug: "366265225" +} + +flag { + name: "viewroot_choreographer" + namespace: "system_performance" + description: "when enabled janktracker will get the instance of choreographer from viewrootimpl" + bug: "377960907" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 8e6b88c66408..5c267c9f6475 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -255,6 +255,16 @@ flag { } flag { + name: "redaction_on_lockscreen_metrics" + namespace: "systemui" + description: "enables metrics when redacting notifications on the lockscreen" + bug: "343631648" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "api_rich_ongoing" is_exported: true namespace: "systemui" diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 0085e4f42397..4fb3982c3754 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -150,3 +150,11 @@ flag { description: "Settings override for virtual devices" bug: "371801645" } + +flag { + namespace: "virtual_devices" + name: "viewconfiguration_apis" + description: "APIs for settings ViewConfiguration attributes on virtual devices" + bug: "370720522" + is_exported: true +} diff --git a/core/java/android/hardware/usb/IUsbManagerInternal.aidl b/core/java/android/hardware/usb/IUsbManagerInternal.aidl new file mode 100644 index 000000000000..32479d449f24 --- /dev/null +++ b/core/java/android/hardware/usb/IUsbManagerInternal.aidl @@ -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 android.hardware.usb; + +import android.hardware.usb.IUsbOperationInternal; + +/** @hide */ +interface IUsbManagerInternal { + + /* Disable/enable USB data on a port for System Service callers. */ + boolean enableUsbDataSignal(boolean enable, int disableReason); +} diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 7b47efd47008..894b068b1528 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -527,22 +527,13 @@ public class InputMethodService extends AbstractInputMethodService { public static final int IME_ACTIVE = 1 << 0; /** - * The IME is perceptibly visible to the user. + * The IME is visible. * * @hide */ public static final int IME_VISIBLE = 1 << 1; /** - * The IME is visible, but not yet perceptible to the user (e.g. fading in) - * by {@link android.view.WindowInsetsController}. - * - * @see InputMethodManager#reportPerceptible - * @hide - */ - public static final int IME_VISIBLE_IMPERCEPTIBLE = 1 << 2; - - /** * The IME window visibility state. * * @hide @@ -550,7 +541,6 @@ public class InputMethodService extends AbstractInputMethodService { @IntDef(flag = true, prefix = { "IME_" }, value = { IME_ACTIVE, IME_VISIBLE, - IME_VISIBLE_IMPERCEPTIBLE, }) public @interface ImeWindowVisibility {} diff --git a/core/java/android/os/ExternalVibration.java b/core/java/android/os/ExternalVibration.java index 3aad0fd7b82f..70a17ab00235 100644 --- a/core/java/android/os/ExternalVibration.java +++ b/core/java/android/os/ExternalVibration.java @@ -87,8 +87,12 @@ public class ExternalVibration implements Parcelable { int capturePreset = in.readInt(); int flags = in.readInt(); AudioAttributes.Builder builder = new AudioAttributes.Builder(); - return builder.setUsage(usage) - .setContentType(contentType) + if (AudioAttributes.isSystemUsage(usage)) { + builder.setSystemUsage(usage); + } else { + builder.setUsage(usage); + } + return builder.setContentType(contentType) .setCapturePreset(capturePreset) .setFlags(flags) .build(); @@ -196,7 +200,9 @@ public class ExternalVibration implements Parcelable { } private static void writeAudioAttributes(AudioAttributes attrs, Parcel out) { - out.writeInt(attrs.getUsage()); + // Since we allow audio system usages, must use getSystemUsage() instead of getUsage() for + // all usages. + out.writeInt(attrs.getSystemUsage()); out.writeInt(attrs.getContentType()); out.writeInt(attrs.getCapturePreset()); out.writeInt(attrs.getAllFlags()); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1210790668de..b97c9b5e83f2 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9465,24 +9465,6 @@ public final class Settings { "reduce_bright_colors_persist_across_reboots"; /** - * Setting that specifies whether Even Dimmer - a feature that allows the brightness - * slider to go below what the display can conventionally do, should be enabled. - * - * @hide - */ - public static final String EVEN_DIMMER_ACTIVATED = - "even_dimmer_activated"; - - /** - * Setting that specifies which nits level Even Dimmer should allow the screen brightness - * to go down to. - * - * @hide - */ - public static final String EVEN_DIMMER_MIN_NITS = - "even_dimmer_min_nits"; - - /** * Setting that holds EM_VALUE (proprietary) * * @hide @@ -10601,6 +10583,9 @@ public final class Settings { * * @hide */ + @TestApi + @Readable + @SuppressLint({"UnflaggedApi", "NoSettingsProvider"}) // @TestApi purely for CTS support. public static final String GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"; /** diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index f7f4eeca58e2..7d7087642fad 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -4988,6 +4988,66 @@ public final class Telephony { public static final String COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM = "is_satellite_provisioned_for_non_ip_datagram"; + /** + * TelephonyProvider column name for satellite entitlement barred plmns list separated by + * comma [,]. The value of this column is set based on entitlement query result for + * satellite configuration. Ex : 31026,302820,40445 + * By default, it's empty. + * + * @hide + */ + public static final String COLUMN_SATELLITE_ENTITLEMENT_BARRED_PLMNS = + "satellite_entitlement_barred_plmns"; + + + /** + * TelephonyProvider column name for satellite entitlement data plan for plmns which is + * built in Json format in Key:Value pair. The value of this column is set based on + * entitlement query result for satellite configuration. + * Ex : {"302820":0,"31026":1, "40445":0} + * By default, it's empty. + * + * @hide + */ + public static final String COLUMN_SATELLITE_ENTITLEMENT_DATA_PLAN_PLMNS = + "satellite_entitlement_data_plan_plmns"; + + /** + * TelephonyProvider column name for satellite entitlement service type map which is + * built in Json format in Key:Value pair. The value of this column is set based on + * entitlement query result for satellite configuration. + * Ex : {"302820":[1,3],"31026":[2,3],"40445":[1,3]} + * By default, it's empty. + * + * @hide + */ + public static final String COLUMN_SATELLITE_ENTITLEMENT_SERVICE_TYPE_MAP = + "satellite_entitlement_service_type_map"; + + /** + * TelephonyProvider column name for satellite entitlement data service policy type map + * which is built in Json format in Key:Value pair. The value of this column is set based + * on entitlement query result for satellite configuration. + * Ex : {"302820":2, "31026":1} + * By default, it's empty. + * + * @hide + */ + public static final String COLUMN_SATELLITE_ENTITLEMENT_DATA_SERVICE_POLICY = + "satellite_entitlement_data_service_policy"; + + /** + * TelephonyProvider column name for satellite entitlement voice service policy type map + * which is built in Json format in Key:Value pair. The value of this column is set + * based on entitlement query result for satellite configuration. + * Ex : {"302820":2, "31026":1}. + * By default, it's empty. + * + * @hide + */ + public static final String COLUMN_SATELLITE_ENTITLEMENT_VOICE_SERVICE_POLICY = + "satellite_entitlement_voice_service_policy"; + /** All columns in {@link SimInfo} table. */ private static final List<String> ALL_COLUMNS = List.of( COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID, @@ -5065,7 +5125,12 @@ public final class Telephony { COLUMN_SATELLITE_ENTITLEMENT_STATUS, COLUMN_SATELLITE_ENTITLEMENT_PLMNS, COLUMN_SATELLITE_ESOS_SUPPORTED, - COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM + COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM, + COLUMN_SATELLITE_ENTITLEMENT_BARRED_PLMNS, + COLUMN_SATELLITE_ENTITLEMENT_DATA_PLAN_PLMNS, + COLUMN_SATELLITE_ENTITLEMENT_SERVICE_TYPE_MAP, + COLUMN_SATELLITE_ENTITLEMENT_DATA_SERVICE_POLICY, + COLUMN_SATELLITE_ENTITLEMENT_VOICE_SERVICE_POLICY ); /** diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 3a3ea189b227..7013f7d705f8 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -52,13 +52,6 @@ flag { } flag { - name: "deprecate_fsv_sig" - namespace: "hardware_backed_security" - description: "Feature flag for deprecating .fsv_sig" - bug: "277916185" -} - -flag { name: "extend_vb_chain_to_updated_apk" namespace: "hardware_backed_security" description: "Use v4 signature and fs-verity to chain verification of allowlisted APKs to Verified Boot" diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java index 505db30ca719..772fd968e507 100644 --- a/core/java/android/service/notification/Adjustment.java +++ b/core/java/android/service/notification/Adjustment.java @@ -225,7 +225,7 @@ public final class Adjustment implements Parcelable { public static final int TYPE_CONTENT_RECOMMENDATION = 4; /** - * Data type: String, a summarization of the text of the notification, or, if provided for + * Data type: CharSequence, a summarization of the text of the notification, or, if provided for * a group summary, a summarization of the text of all of the notificatrions in the group. * Send this key with a null value to remove an existing summarization. */ diff --git a/core/java/android/view/NotificationTopLineView.java b/core/java/android/view/NotificationTopLineView.java index e567414c9b8a..beaf2118d4dc 100644 --- a/core/java/android/view/NotificationTopLineView.java +++ b/core/java/android/view/NotificationTopLineView.java @@ -385,6 +385,13 @@ public class NotificationTopLineView extends ViewGroup { } /** + * Returns whether the title is present. + */ + public boolean isTitlePresent() { + return mTitle != null; + } + + /** * Determine if the given point is touching an active part of the top line. */ public boolean isInTouchRect(float x, float y) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9d0773f0a606..7dc96f21b5ae 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1836,6 +1836,7 @@ public final class ViewRootImpl implements ViewParent, eventsToBeRegistered, mBasePackageName); + // LINT.IfChange(fi_cb) if (forceInvertColor()) { if (mForceInvertObserver == null) { mForceInvertObserver = new ContentObserver(mHandler) { @@ -1844,7 +1845,6 @@ public final class ViewRootImpl implements ViewParent, updateForceDarkMode(); } }; - final Uri[] urisToObserve = { Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED), @@ -1859,6 +1859,7 @@ public final class ViewRootImpl implements ViewParent, } } } + // LINT.ThenChange(/services/core/java/com/android/server/UiModeManagerService.java:fi_cb) } /** @@ -13654,4 +13655,11 @@ public final class ViewRootImpl implements ViewParent, ThreadedRenderer.preInitBufferAllocator(); } } + + /** + * @hide + */ + public Choreographer getChoreographer() { + return mChoreographer; + } } diff --git a/core/java/android/view/XrWindowProperties.java b/core/java/android/view/XrWindowProperties.java new file mode 100644 index 000000000000..23021a563393 --- /dev/null +++ b/core/java/android/view/XrWindowProperties.java @@ -0,0 +1,159 @@ +/* + * 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 android.view; + +import android.annotation.FlaggedApi; + +/** + * Class for XR-specific window properties to put in application manifests. + */ +@FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) +public final class XrWindowProperties { + /** @hide */ + private XrWindowProperties() {} + + /** + * Both Application and activity level + * {@link android.content.pm.PackageManager.Property PackageManager.Property} for an app to + * inform the system of the activity launch mode in XR. When it is declared at the application + * level, all activities are set to the defined value, unless it is overridden at the activity + * level. + * + * <p>The default value is {@link #XR_ACTIVITY_START_MODE_UNDEFINED}. + * + * <p>The available values are: + * <ul> + * <li>{@link #XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED} + * <li>{@link #XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED} + * <li>{@link #XR_ACTIVITY_START_MODE_HOME_SPACE} + * <li>{@link #XR_ACTIVITY_START_MODE_UNDEFINED} + * </ul> + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_ACTIVITY_XR_START_MODE" + * android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED| + * XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED| + * XR_ACTIVITY_START_MODE_HOME_SPACE| + * XR_ACTIVITY_START_MODE_UNDEFINED"/> + * </application> + * </pre> + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String PROPERTY_XR_ACTIVITY_START_MODE = + "android.window.PROPERTY_XR_ACTIVITY_START_MODE"; + + /** + * Defines the value to launch an activity in unmanaged full space mode in XR, where the + * activity itself is rendering the space and controls its own scene graph. This should be used + * for all activities that use OpenXR to render. + * + * @see #PROPERTY_XR_ACTIVITY_START_MODE + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED = + "XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED"; + + /** + * The default value if not specified. If used, the actual launching mode will be determined by + * the system based on the launching activity's current mode and the launching flags. When + * {@link #PROPERTY_XR_ACTIVITY_START_MODE} is used at the application level, apps can use this + * value to reset at individual activity level. + * + * @see #PROPERTY_XR_ACTIVITY_START_MODE + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String XR_ACTIVITY_START_MODE_UNDEFINED = + "XR_ACTIVITY_START_MODE_UNDEFINED"; + + /** + * Defines the value to launch an activity in + * <a href="https://developer.android.com/develop/xr/jetpack-xr-sdk/transition-home-space-to-full-space">managed + * full space mode</a> in XR, where the system is rendering the activity from a scene graph. + * + * @see #PROPERTY_XR_ACTIVITY_START_MODE + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED = + "XR_ACTIVITY_START_MODE_FULL_SPACE_MANAGED"; + + /** + * Defines the value to launch an activity in + * <a href="https://developer.android.com/develop/xr/jetpack-xr-sdk/transition-home-space-to-full-space">home + * space mode</a> in XR. + * + * @see #PROPERTY_XR_ACTIVITY_START_MODE + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String XR_ACTIVITY_START_MODE_HOME_SPACE = + "XR_ACTIVITY_START_MODE_HOME_SPACE"; + + /** + * Both Application and activity level + * {@link android.content.pm.PackageManager.Property PackageManager.Property} for an app to + * inform the system of the type of safety boundary recommended for the activity. When it is + * declared at the application level, all activities are set to the defined value, unless it is + * overridden at the activity level. When not declared, the system will not enforce any + * recommendations for a type of safety boundary and will continue to use the type that is + * currently in use. + * + * <p>The default value is {@link #XR_BOUNDARY_TYPE_NO_RECOMMENDATION}. + * + * <p>The available values are: + * <ul> + * <li>{@link #XR_BOUNDARY_TYPE_LARGE} + * <li>{@link #XR_BOUNDARY_TYPE_NO_RECOMMENDATION} + * </ul> + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED" + * android:value="XR_BOUNDARY_TYPE_LARGE| + * XR_BOUNDARY_TYPE_NO_RECOMMENDATION"/> + * </application> + * </pre> + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED = + "android.window.PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED"; + + /** + * Defines the value to launch an activity with no recommendations for the type of safety + * boundary. The system will continue to use the type of safety boundary that is currently + * in use. + * + * @see #PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String XR_BOUNDARY_TYPE_NO_RECOMMENDATION = + "XR_BOUNDARY_TYPE_NO_RECOMMENDATION"; + + /** + * Defines the value to launch an activity with a large boundary recommended. This is useful for + * activities which expect users to be moving around. The system will ask the user to use a + * larger size for their safety boundary and check that their space is clear, if the larger + * size is not already in use. This larger size will be determined by the system. + * + * @see #PROPERTY_XR_BOUNDARY_TYPE_RECOMMENDED + */ + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + public static final String XR_BOUNDARY_TYPE_LARGE = "XR_BOUNDARY_TYPE_LARGE"; +} diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java index e0c48b03dad8..cf582176a9f7 100644 --- a/core/java/android/window/DesktopExperienceFlags.java +++ b/core/java/android/window/DesktopExperienceFlags.java @@ -60,6 +60,8 @@ public enum DesktopExperienceFlags { ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, false), ENABLE_MULTIPLE_DESKTOPS_BACKEND(Flags::enableMultipleDesktopsBackend, false), ENABLE_MULTIPLE_DESKTOPS_FRONTEND(Flags::enableMultipleDesktopsFrontend, false), + ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS( + Flags::enablePersistingDisplaySizeForConnectedDisplays, false), ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY(Flags::enablePerDisplayDesktopWallpaperActivity, false), ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF( diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 1b8f73a7edb8..888d7e7c895d 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -53,12 +53,14 @@ public enum DesktopModeFlags { ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS( Flags::enableCaptionCompatInsetForceConsumptionAlways, true), ENABLE_CASCADING_WINDOWS(Flags::enableCascadingWindows, true), + ENABLE_DESKTOP_APP_HANDLE_ANIMATION(Flags::enableDesktopAppHandleAnimation, false), ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX( Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix, true), ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false), ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true), + ENABLE_DESKTOP_IMMERSIVE_DRAG_BUGFIX(Flags::enableDesktopImmersiveDragBugfix, false), ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX( Flags::enableDesktopIndicatorInSeparateThreadBugfix, false), ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX( @@ -106,6 +108,7 @@ public enum DesktopModeFlags { ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true), ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true), ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), + ENABLE_INPUT_LAYER_TRANSITION_FIX(Flags::enableInputLayerTransitionFix, false), ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true), ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS(Flags::enableModalsFullscreenWithPermission, false), ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS( diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 6edab295ee53..1f710c1cc8c0 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -154,6 +154,16 @@ flag { } flag { + name: "enable_input_layer_transition_fix" + namespace: "lse_desktop_experience" + description: "Enables a bugfix for input layer disposal during certain transitions." + bug: "371473978" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_accessible_custom_headers" namespace: "lse_desktop_experience" description: "Enables a11y-friendly custom header input handling" @@ -803,6 +813,26 @@ flag { } flag { + name: "enable_desktop_app_handle_animation" + namespace: "lse_desktop_experience" + description: "Enables the animation that occurs when the app handle of a task in immersive mode is shown or hidden." + bug: "375252977" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "enable_desktop_immersive_drag_bugfix" + namespace: "lse_desktop_experience" + description: "Keeps the app handle visible during a drag." + bug: "381280828" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_desktop_indicator_in_separate_thread_bugfix" namespace: "lse_desktop_experience" description: "Enables running visual indicator view operations in ShellDesktopThread." @@ -827,13 +857,10 @@ flag { } flag { - name: "enable_persisting_density_scale_for_connected_displays" + name: "enable_persisting_display_size_for_connected_displays" namespace: "lse_desktop_experience" - description: "Enables persisting density scale on resolution change for connected displays." + description: "Enables persisting display size on resolution change for connected displays." bug: "392855657" - metadata { - purpose: PURPOSE_BUGFIX - } } flag { diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index f2ba16cf43d6..b4fec416bd5f 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -51,6 +51,17 @@ flag { } flag { + name: "use_cached_insets_for_display_switch" + namespace: "windowing_frontend" + description: "Reduce intermediate insets changes for display switch" + bug: "266197298" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "edge_to_edge_by_default" namespace: "windowing_frontend" description: "Make app go edge-to-edge by default when targeting SDK 35 or greater" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index f2efa200918c..cbeaeda65fca 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -81,6 +81,16 @@ flag { flag { namespace: "windowing_sdk" + name: "activity_embedding_delay_task_fragment_finish_for_activity_launch" + description: "Fixes a race condition that we finish the TaskFragment too early when there is a pending activity launch." + bug: "390452023" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + namespace: "windowing_sdk" name: "wlinfo_oncreate" description: "Makes WindowLayoutInfo accessible without racing in the Activity#onCreate()" bug: "337820752" @@ -127,17 +137,6 @@ flag { flag { namespace: "windowing_sdk" - name: "use_self_sync_transaction_for_layer" - description: "Always use this.getSyncTransaction for assignLayer" - bug: "388127825" - is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - namespace: "windowing_sdk" name: "safe_region_letterboxing" description: "Enables letterboxing for a safe region" bug: "380132497" diff --git a/core/jni/android_media_ImageWriter.cpp b/core/jni/android_media_ImageWriter.cpp index 1357dd842ff1..8e58922bd9df 100644 --- a/core/jni/android_media_ImageWriter.cpp +++ b/core/jni/android_media_ImageWriter.cpp @@ -399,7 +399,7 @@ static jlong ImageWriter_init(JNIEnv* env, jobject thiz, jobject weakThiz, jobje } sp<JNIImageWriterContext> ctx(new JNIImageWriterContext(env, weakThiz, clazz)); - sp<Surface> producer = new Surface(bufferProducer, /*controlledByApp*/false); + sp<Surface> producer = sp<Surface>::make(bufferProducer, /*controlledByApp*/ false); ctx->setProducer(producer); /** * NATIVE_WINDOW_API_CPU isn't a good choice here, as it makes the bufferQueue not connectable diff --git a/core/jni/android_view_Surface.cpp b/core/jni/android_view_Surface.cpp index 312c2067d396..783daec82b9e 100644 --- a/core/jni/android_view_Surface.cpp +++ b/core/jni/android_view_Surface.cpp @@ -139,7 +139,7 @@ jobject android_view_Surface_createFromIGraphicBufferProducer(JNIEnv* env, return NULL; } - sp<Surface> surface(new Surface(bufferProducer, true)); + sp<Surface> surface = sp<Surface>::make(bufferProducer, true); return android_view_Surface_createFromSurface(env, surface); } @@ -161,7 +161,7 @@ static jlong nativeCreateFromSurfaceTexture(JNIEnv* env, jclass clazz, return 0; } - sp<Surface> surface(new Surface(producer, true)); + sp<Surface> surface = sp<Surface>::make(producer, true); if (surface == NULL) { jniThrowException(env, OutOfResourcesException, NULL); return 0; @@ -358,8 +358,8 @@ static jlong nativeReadFromParcel(JNIEnv* env, jclass clazz, sp<Surface> sur; if (surfaceShim.graphicBufferProducer != nullptr) { // we have a new IGraphicBufferProducer, create a new Surface for it - sur = new Surface(surfaceShim.graphicBufferProducer, true, - surfaceShim.surfaceControlHandle); + sur = sp<Surface>::make(surfaceShim.graphicBufferProducer, true, + surfaceShim.surfaceControlHandle); // and keep a reference before passing to java sur->incStrong(&sRefBaseOwner); } diff --git a/core/jni/android_view_SurfaceSession.cpp b/core/jni/android_view_SurfaceSession.cpp index 6ad109e80752..4f2ab09252c8 100644 --- a/core/jni/android_view_SurfaceSession.cpp +++ b/core/jni/android_view_SurfaceSession.cpp @@ -42,14 +42,14 @@ sp<SurfaceComposerClient> android_view_SurfaceSession_getClient( static jlong nativeCreate(JNIEnv* env, jclass clazz) { - SurfaceComposerClient* client = new SurfaceComposerClient(); - client->incStrong((void*)nativeCreate); - return reinterpret_cast<jlong>(client); + // Will be deleted via decStrong() in nativeDestroy. + auto client = sp<SurfaceComposerClient>::make(); + return reinterpret_cast<jlong>(client.release()); } static void nativeDestroy(JNIEnv* env, jclass clazz, jlong ptr) { SurfaceComposerClient* client = reinterpret_cast<SurfaceComposerClient*>(ptr); - client->decStrong((void*)nativeCreate); + client->decStrong((void*)client); } static const JNINativeMethod gMethods[] = { diff --git a/core/jni/android_view_TextureView.cpp b/core/jni/android_view_TextureView.cpp index 21fe1f020b29..f71878ccff08 100644 --- a/core/jni/android_view_TextureView.cpp +++ b/core/jni/android_view_TextureView.cpp @@ -85,7 +85,7 @@ static void android_view_TextureView_createNativeWindow(JNIEnv* env, jobject tex jobject surface) { sp<IGraphicBufferProducer> producer(SurfaceTexture_getProducer(env, surface)); - sp<ANativeWindow> window = new Surface(producer, true); + sp<ANativeWindow> window = sp<Surface>::make(producer, true); window->incStrong((void*)android_view_TextureView_createNativeWindow); SET_LONG(textureView, gTextureViewClassInfo.nativeWindow, jlong(window.get())); diff --git a/core/jni/com_google_android_gles_jni_EGLImpl.cpp b/core/jni/com_google_android_gles_jni_EGLImpl.cpp index 75330be2624d..1b3b14da49d5 100644 --- a/core/jni/com_google_android_gles_jni_EGLImpl.cpp +++ b/core/jni/com_google_android_gles_jni_EGLImpl.cpp @@ -300,7 +300,7 @@ not_valid_surface: } sp<IGraphicBufferProducer> producer(SurfaceTexture_getProducer(_env, native_window)); - window = new Surface(producer, true); + window = sp<Surface>::make(producer, true); if (window == NULL) goto not_valid_surface; diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index 746740b0248b..1e8da730aff8 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -280,12 +280,18 @@ static string getJavaProperty(JNIEnv* env, const char* property_name, return string(chars.c_str()); } -static void loadIcuData(string icuPath) { +static void loadIcuData(JNIEnv* env, string icuPath) { void* addr = mmapFile(icuPath.c_str()); + if (addr == nullptr) { + jniThrowRuntimeException(env, "Failed to map the ICU data file."); + } UErrorCode err = U_ZERO_ERROR; udata_setCommonData(addr, &err); if (err != U_ZERO_ERROR) { - ALOGE("Unable to load ICU data\n"); + jniThrowRuntimeException(env, + format("udata_setCommonData failed with error code {}", + u_errorName(err)) + .c_str()); } } @@ -296,12 +302,12 @@ static void loadIcuData() { JNIEnv* env = AndroidRuntime::getJNIEnv(); string icuPath = base::GetProperty("ro.icu.data.path", ""); if (!icuPath.empty()) { - loadIcuData(icuPath); + loadIcuData(env, icuPath); } else { // fallback to read from java.lang.System.getProperty string icuPathFromJava = getJavaProperty(env, "icu.data.path"); if (!icuPathFromJava.empty()) { - loadIcuData(icuPathFromJava); + loadIcuData(env, icuPathFromJava); } } diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 8de77469d170..ac4bac6d206e 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -295,13 +295,6 @@ message SecureSettingsProto { optional SettingProto enhanced_voice_privacy_enabled = 23 [ (android.privacy).dest = DEST_AUTOMATIC ]; - message EvenDimmer { - optional SettingProto even_dimmer_activated = 1 [ (android.privacy).dest = DEST_AUTOMATIC ]; - optional SettingProto even_dimmer_min_nits = 2 [ (android.privacy).dest = DEST_AUTOMATIC ]; - } - - optional EvenDimmer even_dimmer = 98; - optional SettingProto font_weight_adjustment = 85 [ (android.privacy).dest = DEST_AUTOMATIC ]; message Gesture { diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index fbb8e25eeced..9773f557dfaa 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4459,6 +4459,10 @@ etc. dialogs. --> <string translatable="false" name="config_appsNotReportingCrashes"></string> + <!-- Specifies the delay in milliseconds for JobScheduler to postpone the running + of regular jobs when coming out of doze --> + <integer name="config_jobSchedulerBackgroundJobsDelay">3000</integer> + <!-- Inactivity threshold (in milliseconds) used in JobScheduler. JobScheduler will consider the device to be "idle" after being inactive for this long. --> <integer name="config_jobSchedulerInactivityIdleThreshold">1860000</integer> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 6245a53b02f5..ef6b9188532e 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -80,11 +80,6 @@ <bool name="auto_data_switch_ping_test_before_switch">true</bool> <java-symbol type="bool" name="auto_data_switch_ping_test_before_switch" /> - <!-- TODO: remove after V --> - <!-- Boolean indicating whether allow to use a roaming nonDDS if user enabled its roaming. --> - <bool name="auto_data_switch_allow_roaming">true</bool> - <java-symbol type="bool" name="auto_data_switch_allow_roaming" /> - <!-- Define the tolerated gap of score for auto data switch decision, larger than which the device will switch to the SIM with higher score. The score is used in conjunction with the score table defined in diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f5424dbed21a..2b7261b62c64 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3016,6 +3016,7 @@ <java-symbol type="integer" name="config_defaultNightMode" /> + <java-symbol type="integer" name="config_jobSchedulerBackgroundJobsDelay" /> <java-symbol type="integer" name="config_jobSchedulerInactivityIdleThreshold" /> <java-symbol type="integer" name="config_jobSchedulerInactivityIdleThresholdOnStablePower" /> <java-symbol type="integer" name="config_jobSchedulerIdleWindowSlop" /> diff --git a/core/tests/vibrator/src/android/os/ExternalVibrationTest.java b/core/tests/vibrator/src/android/os/ExternalVibrationTest.java index 8741907cd5ea..9c9d50202486 100644 --- a/core/tests/vibrator/src/android/os/ExternalVibrationTest.java +++ b/core/tests/vibrator/src/android/os/ExternalVibrationTest.java @@ -49,4 +49,22 @@ public class ExternalVibrationTest { assertThat(restored.getAudioAttributes()).isEqualTo(original.getAudioAttributes()); assertThat(restored.getToken()).isEqualTo(original.getToken()); } + + @Test + public void testSerialization_systemUsage() { + ExternalVibration original = + new ExternalVibration( + 123, + "pkg", + new AudioAttributes.Builder() + .setSystemUsage(AudioAttributes.USAGE_SPEAKER_CLEANUP) + .build(), + IExternalVibrationController.Stub.asInterface(new Binder())); + Parcel p = Parcel.obtain(); + original.writeToParcel(p, 0); + p.setDataPosition(0); + ExternalVibration restored = ExternalVibration.CREATOR.createFromParcel(p); + + assertThat(restored.getAudioAttributes()).isEqualTo(original.getAudioAttributes()); + } } diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index 477d207a5c7e..a75453578f16 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -53,15 +53,12 @@ <com.android.wm.shell.windowdecor.HandleMenuImageButton android:id="@+id/collapse_menu_button" - android:layout_width="16dp" - android:layout_height="16dp" - android:layout_marginEnd="16dp" - android:layout_marginStart="14dp" + android:padding="16dp" android:contentDescription="@string/collapse_menu_text" - android:src="@drawable/ic_baseline_expand_more_24" + android:src="@drawable/ic_baseline_expand_more_16" android:rotation="180" android:tint="@androidprv:color/materialColorOnSurface" - android:background="?android:selectableItemBackgroundBorderless"/> + style="@style/DesktopModeHandleMenuWindowingButton"/> </LinearLayout> <LinearLayout diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 32660e8fca27..60044ad51d83 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -619,13 +619,13 @@ <dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen> <!-- The corner radius of the windowing actions pill buttons's ripple drawable --> - <dimen name="desktop_mode_handle_menu_windowing_action_ripple_radius">24dp</dimen> + <dimen name="desktop_mode_handle_menu_icon_button_ripple_radius">24dp</dimen> <!-- The horizontal/vertical inset to apply to the ripple drawable effect of windowing actions pill central buttons --> - <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_base">2dp</dimen> + <dimen name="desktop_mode_handle_menu_icon_button_ripple_inset_base">2dp</dimen> <!-- The horizontal/vertical vertical inset to apply to the ripple drawable effect of windowing actions pill edge buttons --> - <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_shift">4dp</dimen> + <dimen name="desktop_mode_handle_menu_icon_button_ripple_inset_shift">4dp</dimen> <!-- The corner radius of the minimize button's ripple drawable --> <dimen name="desktop_mode_header_minimize_ripple_radius">18dp</dimen> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index 1a80b0f29aa9..362a5c5c3750 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -111,47 +111,28 @@ class DragZoneFactory( /** Updates all dimensions after a configuration change. */ fun onConfigurationUpdated() { - dismissDragZoneSize = - if (deviceConfig.isSmallTablet) { - context.resolveDimension(R.dimen.drag_zone_dismiss_fold) - } else { - context.resolveDimension(R.dimen.drag_zone_dismiss_tablet) - } - bubbleDragZoneTabletSize = context.resolveDimension(R.dimen.drag_zone_bubble_tablet) - bubbleDragZoneFoldableSize = context.resolveDimension(R.dimen.drag_zone_bubble_fold) - fullScreenDragZoneWidth = context.resolveDimension(R.dimen.drag_zone_full_screen_width) - fullScreenDragZoneHeight = context.resolveDimension(R.dimen.drag_zone_full_screen_height) - desktopWindowDragZoneWidth = - context.resolveDimension(R.dimen.drag_zone_desktop_window_width) - desktopWindowDragZoneHeight = - context.resolveDimension(R.dimen.drag_zone_desktop_window_height) - desktopWindowFromExpandedViewDragZoneWidth = - context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_width) - desktopWindowFromExpandedViewDragZoneHeight = - context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_height) - splitFromBubbleDragZoneHeight = - context.resolveDimension(R.dimen.drag_zone_split_from_bubble_height) - splitFromBubbleDragZoneWidth = - context.resolveDimension(R.dimen.drag_zone_split_from_bubble_width) - hSplitFromExpandedViewDragZoneWidth = - context.resolveDimension(R.dimen.drag_zone_h_split_from_expanded_view_width) - vSplitFromExpandedViewDragZoneWidth = - context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_width) - vSplitFromExpandedViewDragZoneHeightTablet = - context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_tablet) - vSplitFromExpandedViewDragZoneHeightFoldTall = - context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall) - vSplitFromExpandedViewDragZoneHeightFoldShort = - context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short) - fullScreenDropTargetPadding = - context.resolveDimension(R.dimen.drop_target_full_screen_padding) - desktopWindowDropTargetPaddingSmall = - context.resolveDimension(R.dimen.drop_target_desktop_window_padding_small) - desktopWindowDropTargetPaddingLarge = - context.resolveDimension(R.dimen.drop_target_desktop_window_padding_large) - - // TODO b/393172431: Use the shared xml resources once we can easily access them from + // TODO b/396539130: Use the shared xml resources once we can easily access them from // launcher + dismissDragZoneSize = + if (deviceConfig.isSmallTablet) 140.dpToPx() else 200.dpToPx() + bubbleDragZoneTabletSize = 200.dpToPx() + bubbleDragZoneFoldableSize = 140.dpToPx() + fullScreenDragZoneWidth = 512.dpToPx() + fullScreenDragZoneHeight = 44.dpToPx() + desktopWindowDragZoneWidth = 880.dpToPx() + desktopWindowDragZoneHeight = 300.dpToPx() + desktopWindowFromExpandedViewDragZoneWidth = 200.dpToPx() + desktopWindowFromExpandedViewDragZoneHeight = 350.dpToPx() + splitFromBubbleDragZoneHeight = 100.dpToPx() + splitFromBubbleDragZoneWidth = 60.dpToPx() + hSplitFromExpandedViewDragZoneWidth = 60.dpToPx() + vSplitFromExpandedViewDragZoneWidth = 200.dpToPx() + vSplitFromExpandedViewDragZoneHeightTablet = 285.dpToPx() + vSplitFromExpandedViewDragZoneHeightFoldTall = 150.dpToPx() + vSplitFromExpandedViewDragZoneHeightFoldShort = 100.dpToPx() + fullScreenDropTargetPadding = 20.dpToPx() + desktopWindowDropTargetPaddingSmall = 100.dpToPx() + desktopWindowDropTargetPaddingLarge = 130.dpToPx() expandedViewDropTargetWidth = 364.dpToPx() expandedViewDropTargetHeight = 578.dpToPx() expandedViewDropTargetPaddingBottom = 108.dpToPx() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java index 5018fdb615da..8e78686ac13d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java @@ -83,7 +83,11 @@ public class SizeChangeAnimation { private static final int ANIMATION_RESOLUTION = 1000; public SizeChangeAnimation(Rect startBounds, Rect endBounds) { - mAnimation = buildContainerAnimation(startBounds, endBounds); + this(startBounds, endBounds, 1f); + } + + public SizeChangeAnimation(Rect startBounds, Rect endBounds, float initialScale) { + mAnimation = buildContainerAnimation(startBounds, endBounds, initialScale); mSnapshotAnim = buildSnapshotAnimation(startBounds, endBounds); } @@ -167,7 +171,8 @@ public class SizeChangeAnimation { } /** Animation for the whole container (snapshot is inside this container). */ - private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds) { + private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds, + float initialScale) { final long duration = ANIMATION_RESOLUTION; boolean growing = endBounds.width() - startBounds.width() + endBounds.height() - startBounds.height() >= 0; @@ -180,15 +185,27 @@ public class SizeChangeAnimation { final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1); scaleAnim.setDuration(scalePeriod); + long scaleStartOffset = 0; if (!growing) { - scaleAnim.setStartOffset(duration - scalePeriod); + scaleStartOffset = duration - scalePeriod; } + scaleAnim.setStartOffset(scaleStartOffset); animSet.addAnimation(scaleAnim); + + if (initialScale != 1f) { + final Animation initialScaleAnim = new ScaleAnimation(initialScale, 1f, initialScale, + 1f); + initialScaleAnim.setDuration(scalePeriod); + initialScaleAnim.setStartOffset(scaleStartOffset); + animSet.addAnimation(initialScaleAnim); + } + final Animation translateAnim = new TranslateAnimation(startBounds.left, endBounds.left, startBounds.top, endBounds.top); translateAnim.setDuration(duration); animSet.addAnimation(translateAnim); Rect startClip = new Rect(startBounds); + startClip.scale(initialScale); Rect endClip = new Rect(endBounds); startClip.offsetTo(0, 0); endClip.offsetTo(0, 0); 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 be2240286ab1..50e2f4d52bf2 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 @@ -2643,6 +2643,15 @@ public class BubbleController implements ConfigurationChangeListener, mBubbleData.setSelectedBubbleAndExpandStack(bubbleToSelect); } + private void moveBubbleToFullscreen(String key) { + Bubble b = mBubbleData.getBubbleInStackWithKey(key); + if (b == null) { + Log.w(TAG, "can't find bubble with key " + key + " to move to fullscreen"); + return; + } + b.getTaskView().moveToFullscreen(); + } + private boolean isDeviceLocked() { return !mIsStatusBarShade; } @@ -2929,6 +2938,11 @@ public class BubbleController implements ConfigurationChangeListener, } }); } + + @Override + public void moveBubbleToFullscreen(String key) { + mMainExecutor.execute(() -> mController.moveBubbleToFullscreen(key)); + } } private class BubblesImpl implements Bubbles { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index abcdb7e70cec..226ad57e4c66 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -1236,7 +1236,6 @@ public class BubbleData { /** @return the bubble in the stack that matches the provided key. */ @Nullable - @VisibleForTesting(visibility = PRIVATE) public Bubble getBubbleInStackWithKey(String key) { return getBubbleWithPredicate(mBubbles, b -> b.getKey().equals(key)); } 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 338ffe76e6ea..e7e7be9cdf6b 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 @@ -30,6 +30,7 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; +import android.graphics.PointF; import android.graphics.Rect; import android.os.IBinder; import android.util.Slog; @@ -155,28 +156,23 @@ public class BubbleTransitions { * Information about the task when it is being dragged to a bubble */ public static class DragData { - private final Rect mBounds; private final WindowContainerTransaction mPendingWct; private final boolean mReleasedOnLeft; + private final float mTaskScale; + private final PointF mDragPosition; /** - * @param bounds bounds of the dragged task when the drag was released - * @param wct pending operations to be applied when finishing the drag * @param releasedOnLeft true if the bubble was released in the left drop target + * @param taskScale the scale of the task when it was dragged to bubble + * @param dragPosition the position of the task when it was dragged to bubble + * @param wct pending operations to be applied when finishing the drag */ - public DragData(@Nullable Rect bounds, @Nullable WindowContainerTransaction wct, - boolean releasedOnLeft) { - mBounds = bounds; + public DragData(boolean releasedOnLeft, float taskScale, @Nullable PointF dragPosition, + @Nullable WindowContainerTransaction wct) { mPendingWct = wct; mReleasedOnLeft = releasedOnLeft; - } - - /** - * @return bounds of the dragged task when the drag was released - */ - @Nullable - public Rect getBounds() { - return mBounds; + mTaskScale = taskScale; + mDragPosition = dragPosition != null ? dragPosition : new PointF(0, 0); } /** @@ -193,6 +189,20 @@ public class BubbleTransitions { public boolean isReleasedOnLeft() { return mReleasedOnLeft; } + + /** + * @return the scale of the task when it was dragged to bubble + */ + public float getTaskScale() { + return mTaskScale; + } + + /** + * @return position of the task when it was dragged to bubble + */ + public PointF getDragPosition() { + return mDragPosition; + } } /** @@ -347,21 +357,24 @@ public class BubbleTransitions { } mFinishCb = finishCallback; - if (mDragData != null && mDragData.getBounds() != null) { - // Override start bounds with the dragged task bounds - mStartBounds.set(mDragData.getBounds()); + if (mDragData != null) { + mStartBounds.offsetTo((int) mDragData.getDragPosition().x, + (int) mDragData.getDragPosition().y); + startTransaction.setScale(mSnapshot, mDragData.getTaskScale(), + mDragData.getTaskScale()); } // Now update state (and talk to launcher) in parallel with snapshot stuff mBubbleData.notificationEntryUpdated(mBubble, /* suppressFlyout= */ true, /* showInShade= */ false); + final int left = mStartBounds.left - info.getRoot(0).getOffset().x; + final int top = mStartBounds.top - info.getRoot(0).getOffset().y; + startTransaction.setPosition(mTaskLeash, left, top); startTransaction.show(mSnapshot); // Move snapshot to root so that it remains visible while task is moved to taskview startTransaction.reparent(mSnapshot, info.getRoot(0).getLeash()); - startTransaction.setPosition(mSnapshot, - mStartBounds.left - info.getRoot(0).getOffset().x, - mStartBounds.top - info.getRoot(0).getOffset().y); + startTransaction.setPosition(mSnapshot, left, top); startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE); BubbleBarExpandedView bbev = mBubble.getBubbleBarExpandedView(); @@ -416,6 +429,8 @@ public class BubbleTransitions { private void playAnimation(boolean animate) { final TaskViewTaskController tv = mBubble.getTaskView().getController(); final SurfaceControl.Transaction startT = new SurfaceControl.Transaction(); + // Set task position to 0,0 as it will be placed inside the TaskView + startT.setPosition(mTaskLeash, 0, 0); mTaskViewTransitions.prepareOpenAnimation(tv, true /* new */, startT, mFinishT, (ActivityManager.RunningTaskInfo) mTaskInfo, mTaskLeash, mFinishWct); @@ -424,10 +439,12 @@ public class BubbleTransitions { } if (animate) { - mLayerView.animateConvert(startT, mStartBounds, mSnapshot, mTaskLeash, () -> { - mFinishCb.onTransitionFinished(mFinishWct); - mFinishCb = null; - }); + float startScale = mDragData != null ? mDragData.getTaskScale() : 1f; + mLayerView.animateConvert(startT, mStartBounds, startScale, mSnapshot, mTaskLeash, + () -> { + mFinishCb.onTransitionFinished(mFinishWct); + mFinishCb = null; + }); } else { startT.apply(); mFinishCb.onTransitionFinished(mFinishWct); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index ae1b4077098d..079edb3ea8ec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -58,4 +58,6 @@ interface IBubbles { oneway void showExpandedView() = 14; oneway void showDropTarget(in boolean show, in @nullable BubbleBarLocation location) = 15; + + oneway void moveBubbleToFullscreen(in String key) = 16; }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 52f20646fb4a..fa22a3961002 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -580,6 +580,7 @@ public class BubbleBarAnimationHelper { public void animateConvert(BubbleViewProvider expandedBubble, @NonNull SurfaceControl.Transaction startT, @NonNull Rect origBounds, + float origScale, @NonNull SurfaceControl snapshot, @NonNull SurfaceControl taskLeash, @Nullable Runnable afterAnimation) { @@ -599,7 +600,7 @@ public class BubbleBarAnimationHelper { new SizeChangeAnimation( new Rect(origBounds.left - position.x, origBounds.top - position.y, origBounds.right - position.x, origBounds.bottom - position.y), - new Rect(0, 0, size.getWidth(), size.getHeight())); + new Rect(0, 0, size.getWidth(), size.getHeight()), origScale); sca.initialize(bbev, taskLeash, snapshot, startT); Animator a = sca.buildViewAnimator(bbev, tvSf, snapshot, /* onFinish */ (va) -> { @@ -614,6 +615,7 @@ public class BubbleBarAnimationHelper { bbev.setSurfaceZOrderedOnTop(true); a.setDuration(EXPANDED_VIEW_ANIMATE_TO_REST_DURATION); + a.setInterpolator(Interpolators.EMPHASIZED); a.start(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 99082f18492c..6c840f020f90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -423,13 +423,13 @@ public class BubbleBarLayerView extends FrameLayout * @param startT A transaction with first-frame work. this *will* be applied here! */ public void animateConvert(@NonNull SurfaceControl.Transaction startT, - @NonNull Rect startBounds, @NonNull SurfaceControl snapshot, SurfaceControl taskLeash, - Runnable animFinish) { + @NonNull Rect startBounds, float startScale, @NonNull SurfaceControl snapshot, + SurfaceControl taskLeash, Runnable animFinish) { if (!mIsExpanded || mExpandedBubble == null) { throw new IllegalStateException("Can't animateExpand without expanded state"); } - mAnimationHelper.animateConvert(mExpandedBubble, startT, startBounds, snapshot, taskLeash, - animFinish); + mAnimationHelper.animateConvert(mExpandedBubble, startT, startBounds, startScale, snapshot, + taskLeash, animFinish); } /** 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 b7867d0b81b5..e20a3d839def 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 @@ -662,14 +662,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return; } - // Check to see if insets changed in such a way that the divider algorithm needs to be - // recalculated. + // Check to see if insets changed in such a way that the divider needs to be animated to + // a new position. (We only do this when switching to pinned taskbar mode and back). Insets pinnedTaskbarInsets = calculatePinnedTaskbarInsets(insetsState); if (!mPinnedTaskbarInsets.equals(pinnedTaskbarInsets)) { mPinnedTaskbarInsets = pinnedTaskbarInsets; // Refresh the DividerSnapAlgorithm. updateLayouts(); - // If the divider is no longer placed on a snap point, animate it to the nearest one. + // If the divider is no longer placed on a snap point, animate it to the nearest one DividerSnapAlgorithm.SnapTarget snapTarget = findSnapTarget(mDividerPosition, 0, false /* hardDismiss */); if (snapTarget.position != mDividerPosition) { @@ -683,18 +683,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** - * Calculates the insets that might trigger a divider algorithm recalculation. Currently, only - * pinned Taskbar does this, and only when the IME is not showing. + * Calculates the insets that might trigger a divider algorithm recalculation. */ private Insets calculatePinnedTaskbarInsets(InsetsState insetsState) { - if (insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, WindowInsets.Type.ime())) { - return Insets.NONE; - } - - // If IME is not showing... for (int i = insetsState.sourceSize() - 1; i >= 0; i--) { final InsetsSource source = insetsState.sourceAt(i); - // and Taskbar is pinned... + // If Taskbar is pinned... if (source.getType() == WindowInsets.Type.navigationBars() && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) { // Return Insets representing the pinned taskbar state. 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 6b7f311daa7c..43a19ef034c9 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 @@ -36,6 +36,7 @@ import android.os.Handler; import android.os.UserManager; import android.view.Choreographer; import android.view.IWindowManager; +import android.view.SurfaceControl; import android.view.WindowManager; import android.window.DesktopModeFlags; @@ -93,6 +94,7 @@ import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopModeKeyGestureHandler; import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; +import com.android.wm.shell.desktopmode.DesktopModeMoveToDisplayTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger; import com.android.wm.shell.desktopmode.DesktopTaskChangeListener; import com.android.wm.shell.desktopmode.DesktopTasksController; @@ -772,7 +774,8 @@ public abstract class WMShellModule { DesksTransitionObserver desksTransitionObserver, UserProfileContexts userProfileContexts, DesktopModeCompatPolicy desktopModeCompatPolicy, - DragToDisplayTransitionHandler dragToDisplayTransitionHandler) { + DragToDisplayTransitionHandler dragToDisplayTransitionHandler, + DesktopModeMoveToDisplayTransitionHandler moveToDisplayTransitionHandler) { return new DesktopTasksController( context, shellInit, @@ -812,7 +815,8 @@ public abstract class WMShellModule { desksTransitionObserver, userProfileContexts, desktopModeCompatPolicy, - dragToDisplayTransitionHandler); + dragToDisplayTransitionHandler, + moveToDisplayTransitionHandler); } @WMSingleton @@ -873,8 +877,7 @@ public abstract class WMShellModule { @ShellMainThread Handler handler) { int maxTaskLimit = DesktopModeStatus.getMaxTaskLimit(context); if (!DesktopModeStatus.canEnterDesktopMode(context) - || !ENABLE_DESKTOP_WINDOWING_TASK_LIMIT.isTrue() - || maxTaskLimit <= 0) { + || !ENABLE_DESKTOP_WINDOWING_TASK_LIMIT.isTrue()) { return Optional.empty(); } return Optional.of( @@ -882,7 +885,7 @@ public abstract class WMShellModule { transitions, desktopUserRepositories, shellTaskOrganizer, - maxTaskLimit, + maxTaskLimit <= 0 ? null : maxTaskLimit, interactionJankMonitor, context, handler)); @@ -950,6 +953,12 @@ public abstract class WMShellModule { @WMSingleton @Provides + static DesktopModeMoveToDisplayTransitionHandler provideMoveToDisplayTransitionHandler() { + return new DesktopModeMoveToDisplayTransitionHandler(new SurfaceControl.Transaction()); + } + + @WMSingleton + @Provides static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler( Context context, Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt new file mode 100644 index 000000000000..fbf170f13a40 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt @@ -0,0 +1,96 @@ +/* + * 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 + +import android.animation.Animator +import android.animation.ValueAnimator +import android.os.IBinder +import android.view.Choreographer +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.wm.shell.shared.animation.Interpolators +import com.android.wm.shell.transition.Transitions +import kotlin.time.Duration.Companion.milliseconds + +/** + * Transition handler for moving a window to a different display. + */ +class DesktopModeMoveToDisplayTransitionHandler( + private val animationTransaction: SurfaceControl.Transaction +) : Transitions.TransitionHandler { + + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo, + ): WindowContainerTransaction? = null + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + val change = info.changes.find { it.startDisplayId != it.endDisplayId } ?: return false + ValueAnimator.ofFloat(0f, 1f) + .apply { + duration = ANIM_DURATION.inWholeMilliseconds + interpolator = Interpolators.LINEAR + addUpdateListener { animation -> + animationTransaction + .setAlpha(change.leash, animation.animatedValue as Float) + .setFrameTimeline(Choreographer.getInstance().vsyncId) + .apply() + } + addListener( + object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator) { + val endBounds = change.endAbsBounds + startTransaction + .setPosition( + change.leash, + endBounds.left.toFloat(), + endBounds.top.toFloat(), + ) + .setWindowCrop(change.leash, endBounds.width(), endBounds.height()) + .apply() + } + + override fun onAnimationEnd(animation: Animator) { + finishTransaction.apply() + finishCallback.onTransitionFinished(null) + } + + override fun onAnimationCancel(animation: Animator) { + finishTransaction.apply() + finishCallback.onTransitionFinished(null) + } + + override fun onAnimationRepeat(animation: Animator) = Unit + } + ) + } + .start() + return true + } + + private companion object { + val ANIM_DURATION = 100.milliseconds + } +} 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 56de48daf810..70539902f651 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 @@ -103,6 +103,10 @@ public class DesktopModeVisualIndicator { return null; } } + + private static boolean isDragToDesktopStartState(DragStartState startState) { + return startState == FROM_FULLSCREEN || startState == FROM_SPLIT; + } } private final VisualIndicatorViewContainer mVisualIndicatorViewContainer; @@ -125,7 +129,12 @@ public class DesktopModeVisualIndicator { @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider, SnapEventHandler snapEventHandler) { SurfaceControl.Builder builder = new SurfaceControl.Builder(); - taskDisplayAreaOrganizer.attachToDisplayArea(taskInfo.displayId, builder); + if (!DragStartState.isDragToDesktopStartState(dragStartState) + || !DesktopModeFlags.ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX.isTrue()) { + // In the DragToDesktop transition we attach the indicator to the transition root once + // that is available - for all other cases attach the indicator here. + taskDisplayAreaOrganizer.attachToDisplayArea(taskInfo.displayId, builder); + } mVisualIndicatorViewContainer = new VisualIndicatorViewContainer( DesktopModeFlags.ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX.isTrue() ? desktopExecutor : mainExecutor, @@ -159,6 +168,18 @@ public class DesktopModeVisualIndicator { mVisualIndicatorViewContainer.releaseVisualIndicator(); } + /** Reparent the visual indicator to {@code newParent}. */ + void reparentLeash(SurfaceControl.Transaction t, SurfaceControl newParent) { + mVisualIndicatorViewContainer.reparentLeash(t, newParent); + } + + /** Start the fade-in animation. */ + void fadeInIndicator() { + mVisualIndicatorViewContainer.fadeInIndicator( + mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, + mTaskInfo.displayId); + } + /** * Based on the coordinates of the current drag event, determine which indicator type we should * display, including no visible indicator. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt index 70a648f57125..fcdf4af531ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt @@ -109,7 +109,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return logD("onTaskMovingToBack for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId) - // TODO: b/367268953 - Connect this with DesktopRepository. + desktopRepository.updateTask(taskInfo.displayId, taskInfo.taskId, /* isVisible= */ false) } override fun onTaskClosing(taskInfo: RunningTaskInfo) { 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 e1dcf0f1c049..301b79afd537 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 @@ -22,6 +22,7 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityOptions import android.app.KeyguardManager import android.app.PendingIntent +import android.app.TaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -208,6 +209,7 @@ class DesktopTasksController( private val userProfileContexts: UserProfileContexts, private val desktopModeCompatPolicy: DesktopModeCompatPolicy, private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler, + private val moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -520,7 +522,8 @@ class DesktopTasksController( return false } logV("moveBackgroundTaskToDesktop with taskId=%d", taskId) - val taskIdToMinimize = bringDesktopAppsToFront(task.displayId, wct, taskId) + val deskId = getDefaultDeskId(task.displayId) + val runOnTransitStart = addDeskActivationChanges(deskId, wct, task) val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, @@ -549,9 +552,7 @@ class DesktopTasksController( desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( FREEFORM_ANIMATION_DURATION ) - taskIdToMinimize?.let { - addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT) - } + runOnTransitStart?.invoke(transition) exitResult.asExit()?.runOnTransitionStart?.invoke(transition) return true } @@ -585,7 +586,7 @@ class DesktopTasksController( reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, ) - val taskIdToMinimize = addDeskActivationWithMovingTaskChanges(deskId, wct, task) + val runOnTransitStart = addDeskActivationWithMovingTaskChanges(deskId, wct, task) val transition: IBinder if (remoteTransition != null) { @@ -600,20 +601,9 @@ class DesktopTasksController( desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( FREEFORM_ANIMATION_DURATION ) - taskIdToMinimize?.let { - addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT) - } + runOnTransitStart?.invoke(transition) exitResult.asExit()?.runOnTransitionStart?.invoke(transition) - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - desksTransitionObserver.addPendingTransition( - DeskTransition.ActiveDeskWithTask( - token = transition, - displayId = displayId, - deskId = deskId, - enterTaskId = task.taskId, - ) - ) - } else { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { taskRepository.setActiveDesk(displayId = displayId, deskId = deskId) } return true @@ -650,6 +640,7 @@ class DesktopTasksController( dragToDesktopTransitionHandler.startDragToDesktopTransition( taskInfo, dragToDesktopValueAnimator, + visualIndicator, ) } @@ -676,7 +667,7 @@ class DesktopTasksController( moveHomeTask(context.displayId, wct) } } - val taskIdToMinimize = addDeskActivationWithMovingTaskChanges(deskId, wct, taskInfo) + val runOnTransitStart = addDeskActivationWithMovingTaskChanges(deskId, wct, taskInfo) val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, @@ -689,20 +680,9 @@ class DesktopTasksController( DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS.toInt() ) if (transition != null) { - taskIdToMinimize?.let { - addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT) - } + runOnTransitStart?.invoke(transition) exitResult.asExit()?.runOnTransitionStart?.invoke(transition) - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - desksTransitionObserver.addPendingTransition( - DeskTransition.ActiveDeskWithTask( - token = transition, - displayId = taskInfo.displayId, - deskId = deskId, - enterTaskId = taskInfo.taskId, - ) - ) - } else { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { taskRepository.setActiveDesk(displayId = taskInfo.displayId, deskId = deskId) } } else { @@ -1057,19 +1037,14 @@ class DesktopTasksController( ?: getDefaultDeskId(displayId) ) val activateDeskWct = WindowContainerTransaction() - addDeskActivationChanges(deskIdToActivate, activateDeskWct) + // TODO: b/391485148 - pass in the launching task here to apply task-limit policy, + // but make sure to not do it twice since it is also done at the start of this + // function. + activationRunOnTransitStart = + addDeskActivationChanges(deskIdToActivate, activateDeskWct) // Desk activation must be handled before app launch-related transactions. activateDeskWct.merge(launchTransaction, /* transfer= */ true) launchTransaction = activateDeskWct - activationRunOnTransitStart = { transition -> - desksTransitionObserver.addPendingTransition( - DeskTransition.ActivateDesk( - token = transition, - displayId = displayId, - deskId = deskIdToActivate, - ) - ) - } desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( FREEFORM_ANIMATION_DURATION ) @@ -1228,7 +1203,8 @@ class DesktopTasksController( } else { null } - val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) + val transition = + transitions.startTransition(TRANSIT_CHANGE, wct, moveToDisplayTransitionHandler) deactivationRunnable?.invoke(transition) return } @@ -1255,17 +1231,8 @@ class DesktopTasksController( wct.reparent(task.token, displayAreaInfo.token, /* onTop= */ true) } - addDeskActivationChanges(destinationDeskId, wct) - val activationRunnable: RunOnTransitStart = { transition -> - desksTransitionObserver.addPendingTransition( - DeskTransition.ActiveDeskWithTask( - token = transition, - displayId = displayId, - deskId = destinationDeskId, - enterTaskId = task.taskId, - ) - ) - } + // TODO: b/391485148 - pass in the moving-to-desk |task| here to apply task-limit policy. + val activationRunnable = addDeskActivationChanges(destinationDeskId, wct) if (Flags.enableDisplayFocusInShellTransitions()) { // Bring the destination display to top with includingParents=true, so that the @@ -1296,7 +1263,8 @@ class DesktopTasksController( } else { null } - val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) + val transition = + transitions.startTransition(TRANSIT_CHANGE, wct, moveToDisplayTransitionHandler) deactivationRunnable?.invoke(transition) activationRunnable?.invoke(transition) } @@ -1661,7 +1629,10 @@ class DesktopTasksController( } } - @Deprecated("Use activeDesk() instead.", ReplaceWith("activateDesk()")) + @Deprecated( + "Use addDeskActivationChanges() instead.", + ReplaceWith("addDeskActivationChanges()"), + ) private fun bringDesktopAppsToFront( displayId: Int, wct: WindowContainerTransaction, @@ -2275,7 +2246,9 @@ class DesktopTasksController( runOnTransitStart?.invoke(transition) return wct } - bringDesktopAppsToFront(task.displayId, wct, task.taskId) + val deskId = getDefaultDeskId(task.displayId) + val runOnTransitStart = addDeskActivationChanges(deskId, wct, task) + runOnTransitStart?.invoke(transition) wct.reorder(task.token, true) return wct } @@ -2338,25 +2311,42 @@ class DesktopTasksController( return WindowContainerTransaction().also { wct -> val deskId = getDefaultDeskId(task.displayId) addMoveToDeskTaskChanges(wct = wct, task = task, deskId = deskId) - // In some launches home task is moved behind new task being launched. Make sure - // that's not the case for launches in desktop. Also, if this launch is the first - // one to trigger the desktop mode (e.g., when [forceEnterDesktop()]), activate the - // desktop mode here. - if ( - task.baseIntent.flags.and(Intent.FLAG_ACTIVITY_TASK_ON_HOME) != 0 || - !isDesktopModeShowing(task.displayId) - ) { - bringDesktopAppsToFront(task.displayId, wct, task.taskId) - wct.reorder(task.token, true) - } - - // Desktop Mode is already showing and we're launching a new Task - we might need to - // minimize another Task. - val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId) - taskIdToMinimize?.let { - addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT) - } - addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) + val runOnTransitStart: RunOnTransitStart? = + if ( + task.baseIntent.flags.and(Intent.FLAG_ACTIVITY_TASK_ON_HOME) != 0 || + !isDesktopModeShowing(task.displayId) + ) { + // In some launches home task is moved behind new task being launched. Make + // sure that's not the case for launches in desktop. Also, if this launch is + // the first one to trigger the desktop mode (e.g., when + // [forceEnterDesktop()]), activate the desk here. + val activationRunnable = + addDeskActivationChanges( + deskId = deskId, + wct = wct, + newTask = task, + addPendingLaunchTransition = true, + ) + wct.reorder(task.token, true) + activationRunnable + } else { + { transition: IBinder -> + // The desk was already showing and we're launching a new Task - we + // might need to minimize another Task. + val taskIdToMinimize = + addAndGetMinimizeChanges(task.displayId, wct, task.taskId) + taskIdToMinimize?.let { minimizingTaskId -> + addPendingMinimizeTransition( + transition, + minimizingTaskId, + MinimizeReason.TASK_LIMIT, + ) + } + // Also track the pending launching task. + addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) + } + } + runOnTransitStart?.invoke(transition) desktopImmersiveController.exitImmersiveIfApplicable( transition, wct, @@ -2478,10 +2468,10 @@ class DesktopTasksController( deskId: Int, wct: WindowContainerTransaction, task: RunningTaskInfo, - ): Int? { - val taskIdToMinimize = addDeskActivationChanges(deskId, wct, task) + ): RunOnTransitStart? { + val runOnTransitStart = addDeskActivationChanges(deskId, wct, task) addMoveToDeskTaskChanges(wct = wct, task = task, deskId = deskId) - return taskIdToMinimize + return runOnTransitStart } /** @@ -2751,36 +2741,74 @@ class DesktopTasksController( activateDesk(deskId, remoteTransition) } - /** Activates the given desk but without starting a transition. */ + /** + * Applies the necessary [wct] changes to activate the given desk. + * + * When a task is being brought into a desk together with the activation, then [newTask] is not + * null and may be used to run other desktop policies, such as minimizing another task if the + * task limit has been exceeded. + */ fun addDeskActivationChanges( deskId: Int, wct: WindowContainerTransaction, - newTask: RunningTaskInfo? = null, - ): Int? { + newTask: TaskInfo? = null, + // TODO: b/362720497 - should this be true in other places? Can it be calculated locally + // without having to specify the value? + addPendingLaunchTransition: Boolean = false, + ): RunOnTransitStart? { + logV("addDeskActivationChanges newTaskId=%d deskId=%d", newTask?.taskId, deskId) + val newTaskIdInFront = newTask?.taskId val displayId = taskRepository.getDisplayForDesk(deskId) - return if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - prepareForDeskActivation(displayId, wct) - desksOrganizer.activateDesk(wct, deskId) - if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - // TODO: 362720497 - do non-running tasks need to be restarted with |wct#startTask|? - } - taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( - doesAnyTaskRequireTaskbarRounding(displayId) - ) - // TODO: b/362720497 - activating a desk with the intention to move a new task to - // it means we may need to minimize something in the activating desk. Do so here - // similar to how it's done in #bringDesktopAppsToFront instead of returning null. - null - } else { - bringDesktopAppsToFront(displayId, wct, newTask?.taskId) + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + val taskIdToMinimize = bringDesktopAppsToFront(displayId, wct, newTask?.taskId) + return { transition -> + taskIdToMinimize?.let { minimizingTaskId -> + addPendingMinimizeTransition( + transition = transition, + taskIdToMinimize = minimizingTaskId, + minimizeReason = MinimizeReason.TASK_LIMIT, + ) + } + if (newTask != null && addPendingLaunchTransition) { + addPendingAppLaunchTransition(transition, newTask.taskId, taskIdToMinimize) + } + } + } + prepareForDeskActivation(displayId, wct) + desksOrganizer.activateDesk(wct, deskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + // TODO: 362720497 - do non-running tasks need to be restarted with |wct#startTask|? + } + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( + doesAnyTaskRequireTaskbarRounding(displayId) + ) + // TODO: b/362720497 - activating a desk with the intention to move a new task to + // it means we may need to minimize something in the activating desk. Do so here + // similar to how it's done in #bringDesktopAppsToFront. + return { transition -> + val activateDeskTransition = + if (newTaskIdInFront != null) { + DeskTransition.ActiveDeskWithTask( + token = transition, + displayId = displayId, + deskId = deskId, + enterTaskId = newTaskIdInFront, + ) + } else { + DeskTransition.ActivateDesk( + token = transition, + displayId = displayId, + deskId = deskId, + ) + } + desksTransitionObserver.addPendingTransition(activateDeskTransition) } } /** Activates the given desk. */ fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) { - val displayId = taskRepository.getDisplayForDesk(deskId) val wct = WindowContainerTransaction() - addDeskActivationChanges(deskId, wct) + val runOnTransitStart = addDeskActivationChanges(deskId, wct) val transitionType = transitionType(remoteTransition) val handler = @@ -2790,15 +2818,7 @@ class DesktopTasksController( val transition = transitions.startTransition(transitionType, wct, handler) handler?.setTransition(transition) - if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { - desksTransitionObserver.addPendingTransition( - DeskTransition.ActivateDesk( - token = transition, - displayId = displayId, - deskId = deskId, - ) - ) - } + runOnTransitStart?.invoke(transition) desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( FREEFORM_ANIMATION_DURATION diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index f9ab359e952d..da369f094405 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -38,17 +38,17 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionObserver /** - * Limits the number of tasks shown in Desktop Mode. + * Keeps track of minimized tasks and limits the number of tasks shown in Desktop Mode. * - * This class should only be used if - * [android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT] is enabled and - * [maxTasksLimit] is strictly greater than 0. + * [maxTasksLimit] must be strictly greater than 0 if it's given. + * + * TODO(b/400634379): Separate two responsibilities of this class into two classes. */ class DesktopTasksLimiter( transitions: Transitions, private val desktopUserRepositories: DesktopUserRepositories, private val shellTaskOrganizer: ShellTaskOrganizer, - private val maxTasksLimit: Int, + private val maxTasksLimit: Int?, private val interactionJankMonitor: InteractionJankMonitor, private val context: Context, @ShellMainThread private val handler: Handler, @@ -59,13 +59,19 @@ class DesktopTasksLimiter( private var userId: Int init { - require(maxTasksLimit > 0) { - "DesktopTasksLimiter: maxTasksLimit should be greater than 0. Current value: $maxTasksLimit." + maxTasksLimit?.let { + require(it > 0) { + "DesktopTasksLimiter: maxTasksLimit should be greater than 0. Current value: $it." + } } transitions.registerObserver(minimizeTransitionObserver) userId = ActivityManager.getCurrentUser() desktopUserRepositories.current.addActiveTaskListener(leftoverMinimizedTasksRemover) - logV("Starting limiter with a maximum of %d tasks", maxTasksLimit) + if (maxTasksLimit != null) { + logV("Starting limiter with a maximum of %d tasks", maxTasksLimit) + } else { + logV("Starting limiter without the task limit") + } } data class TaskDetails( @@ -268,6 +274,7 @@ class DesktopTasksLimiter( // If it's a running task, reorder it to back. taskIdToMinimize ?.let { shellTaskOrganizer.getRunningTaskInfo(it) } + // TODO: b/391485148 - this won't really work with multi-desks enabled. ?.let { wct.reorder(it.token, /* onTop= */ false) } return taskIdToMinimize } @@ -324,7 +331,7 @@ class DesktopTasksLimiter( launchingNewIntent: Boolean, ): Int? { val newTasksOpening = if (launchingNewIntent) 1 else 0 - if (visibleOrderedTasks.size + newTasksOpening <= maxTasksLimit) { + if (visibleOrderedTasks.size + newTasksOpening <= (maxTasksLimit ?: Int.MAX_VALUE)) { logV("No need to minimize; tasks below limit") // No need to minimize anything return null 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 d396d8bff2b8..e943c42dcdfc 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 @@ -25,6 +25,7 @@ import android.os.SystemProperties import android.os.UserHandle import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CLOSE +import android.window.DesktopModeFlags import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo @@ -118,6 +119,7 @@ sealed class DragToDesktopTransitionHandler( fun startDragToDesktopTransition( taskInfo: RunningTaskInfo, dragToDesktopAnimator: MoveToDesktopAnimator, + visualIndicator: DesktopModeVisualIndicator?, ) { if (inProgress) { logV("Drag to desktop transition already in progress.") @@ -163,12 +165,14 @@ sealed class DragToDesktopTransitionHandler( dragAnimator = dragToDesktopAnimator, startTransitionToken = startTransitionToken, otherSplitTask = otherTask, + visualIndicator = visualIndicator, ) } else { TransitionState.FromFullscreen( draggedTaskId = taskInfo.taskId, dragAnimator = dragToDesktopAnimator, startTransitionToken = startTransitionToken, + visualIndicator = visualIndicator, ) } } @@ -277,6 +281,7 @@ sealed class DragToDesktopTransitionHandler( val state = requireTransitionState() val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") val animatedTaskBounds = getAnimatedTaskBounds() + state.dragAnimator.cancelAnimator() requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds) } @@ -288,7 +293,6 @@ sealed class DragToDesktopTransitionHandler( val scaledWidth = taskBounds.width() * taskScale val scaledHeight = taskBounds.height() * taskScale val dragPosition = PointF(state.dragAnimator.position) - state.dragAnimator.cancelAnimator() return Rect( dragPosition.x.toInt(), dragPosition.y.toInt(), @@ -321,22 +325,24 @@ sealed class DragToDesktopTransitionHandler( // TODO(b/391928049): update density once we can drag from desktop to bubble val state = requireTransitionState() val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") - val taskBounds = getAnimatedTaskBounds() + val dragPosition = PointF(state.dragAnimator.position) + val scale = state.dragAnimator.scale state.dragAnimator.cancelAnimator() - requestBubble(wct, taskInfo, onLeft, taskBounds) + requestBubble(wct, taskInfo, onLeft, scale, dragPosition) } private fun requestBubble( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo, onLeft: Boolean, - taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds), + taskScale: Float = 1f, + dragPosition: PointF = PointF(0f, 0f), ) { val controller = bubbleController.orElseThrow { IllegalStateException("BubbleController not set") } controller.expandStackAndSelectBubble( taskInfo, - BubbleTransitions.DragData(taskBounds, wct, onLeft), + BubbleTransitions.DragData(onLeft, taskScale, dragPosition, wct), ) } @@ -457,6 +463,13 @@ sealed class DragToDesktopTransitionHandler( state.surfaceLayers = layers state.startTransitionFinishCb = finishCallback state.startTransitionFinishTransaction = finishTransaction + + val taskChange = state.draggedTaskChange ?: error("Expected non-null task change.") + val taskInfo = taskChange.taskInfo ?: error("Expected non-null task info.") + + if (DesktopModeFlags.ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX.isTrue) { + attachIndicatorToTransitionRoot(state, info, taskInfo, startTransaction) + } startTransaction.apply() if (state.cancelState == CancelState.NO_CANCEL) { @@ -485,8 +498,6 @@ sealed class DragToDesktopTransitionHandler( } else { SPLIT_POSITION_BOTTOM_OR_RIGHT } - val taskInfo = - state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.") val wct = WindowContainerTransaction() restoreWindowOrder(wct) state.startTransitionFinishTransaction?.apply() @@ -511,6 +522,21 @@ sealed class DragToDesktopTransitionHandler( return true } + private fun attachIndicatorToTransitionRoot( + state: TransitionState, + info: TransitionInfo, + taskInfo: RunningTaskInfo, + t: SurfaceControl.Transaction, + ) { + val transitionRoot = info.getRoot(info.findRootIndex(taskInfo.displayId)) + state.visualIndicator?.let { + // Attach the indicator to the transition root so that it's removed at the end of the + // transition regardless of whether we managed to release the indicator. + it.reparentLeash(t, transitionRoot.leash) + it.fadeInIndicator() + } + } + /** * Calculates start drag to desktop layers for transition [info]. The leash layer is calculated * based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is @@ -901,6 +927,7 @@ sealed class DragToDesktopTransitionHandler( abstract var surfaceLayers: DragToDesktopLayers? abstract var cancelState: CancelState abstract var startAborted: Boolean + abstract val visualIndicator: DesktopModeVisualIndicator? data class FromFullscreen( override val draggedTaskId: Int, @@ -915,6 +942,7 @@ sealed class DragToDesktopTransitionHandler( override var surfaceLayers: DragToDesktopLayers? = null, override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, + override val visualIndicator: DesktopModeVisualIndicator?, var otherRootChanges: MutableList<Change> = mutableListOf(), ) : TransitionState() @@ -931,6 +959,7 @@ sealed class DragToDesktopTransitionHandler( override var surfaceLayers: DragToDesktopLayers? = null, override var cancelState: CancelState = CancelState.NO_CANCEL, override var startAborted: Boolean = false, + override val visualIndicator: DesktopModeVisualIndicator?, var splitRootChange: Change? = null, var otherSplitTask: Int, ) : TransitionState() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index 5e84019b14f5..1a66ca808dad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -47,6 +47,7 @@ class ToggleResizeDesktopTaskTransitionHandler( private var boundsAnimator: Animator? = null private var initialBounds: Rect? = null + private var callback: (() -> Unit)? = null constructor( transitions: Transitions, @@ -61,9 +62,14 @@ class ToggleResizeDesktopTaskTransitionHandler( * bounds of the actual task). This is provided so that the animation resizing can begin where * the task leash currently is for smoother UX. */ - fun startTransition(wct: WindowContainerTransaction, taskLeashBounds: Rect? = null) { + fun startTransition( + wct: WindowContainerTransaction, + taskLeashBounds: Rect? = null, + callback: (() -> Unit)? = null, + ) { transitions.startTransition(TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE, wct, this) initialBounds = taskLeashBounds + this.callback = callback } fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) { @@ -121,6 +127,8 @@ class ToggleResizeDesktopTaskTransitionHandler( interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW) interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE) + callback?.invoke() + callback = null }, ) addUpdateListener { anim -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt index 919e8164b58e..23562388b3e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt @@ -130,6 +130,12 @@ constructor( } } + /** Reparent the indicator to {@code newParent}. */ + fun reparentLeash(t: SurfaceControl.Transaction, newParent: SurfaceControl) { + val leash = indicatorLeash ?: return + t.reparent(leash, newParent) + } + private fun showIndicator(taskSurface: SurfaceControl, leash: SurfaceControl) { mainExecutor.execute { indicatorLeash = leash @@ -166,7 +172,7 @@ constructor( displayController.getDisplayLayout(taskInfo.displayId) ?: error("Expected to find DisplayLayout for taskId${taskInfo.taskId}.") if (currentType == IndicatorType.NO_INDICATOR) { - fadeInIndicator(layout, newType, taskInfo.displayId, snapEventHandler) + fadeInIndicatorInternal(layout, newType, taskInfo.displayId, snapEventHandler) } else if (newType == IndicatorType.NO_INDICATOR) { fadeOutIndicator( layout, @@ -195,10 +201,22 @@ constructor( } /** + * Fade indicator in as provided type. + * + * Animator fades the indicator in while expanding the bounds outwards. + */ + fun fadeInIndicator(layout: DisplayLayout, type: IndicatorType, displayId: Int) { + if (isReleased) return + desktopExecutor.execute { + fadeInIndicatorInternal(layout, type, displayId, snapEventHandler) + } + } + + /** * Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards. */ @VisibleForTesting - fun fadeInIndicator( + fun fadeInIndicatorInternal( layout: DisplayLayout, type: IndicatorType, displayId: Int, 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 fed336b17f19..65fa9b4b5529 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 @@ -52,6 +52,7 @@ import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.split.SplitScreenConstants; import com.android.wm.shell.transition.OneShotRemoteHandler; import com.android.wm.shell.transition.Transitions; @@ -362,7 +363,8 @@ class SplitScreenTransitions { WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, Transitions.TransitionHandler handler, - int extraTransitType, boolean resizeAnim) { + int extraTransitType, boolean resizeAnim, + @SplitScreenConstants.PersistentSnapPosition int snapPosition) { if (mPendingEnter != null) { ProtoLog.v(WM_SHELL_TRANSITIONS, " splitTransition " + " skip to start enter split transition since it already exist. "); @@ -373,16 +375,18 @@ class SplitScreenTransitions { .onSplitAnimationInvoked(true /*animationRunning*/)); } final IBinder transition = mTransitions.startTransition(transitType, wct, handler); - setEnterTransition(transition, remoteTransition, extraTransitType, resizeAnim); + setEnterTransition(transition, remoteTransition, extraTransitType, resizeAnim, + snapPosition); return transition; } /** Sets a transition to enter split. */ void setEnterTransition(@NonNull IBinder transition, @Nullable RemoteTransition remoteTransition, - int extraTransitType, boolean resizeAnim) { + int extraTransitType, boolean resizeAnim, + int snapPosition) { mPendingEnter = new EnterSession( - transition, remoteTransition, extraTransitType, resizeAnim); + transition, remoteTransition, extraTransitType, resizeAnim, snapPosition); ProtoLog.v(WM_SHELL_TRANSITIONS, " splitTransition " + " deduced Enter split screen"); @@ -675,13 +679,16 @@ class SplitScreenTransitions { /** Bundled information of enter transition. */ class EnterSession extends TransitSession { final boolean mResizeAnim; + /** The starting snap position we'll enter into with this transition. */ + final @SplitScreenConstants.PersistentSnapPosition int mEnteringPosition; EnterSession(IBinder transition, @Nullable RemoteTransition remoteTransition, - int extraTransitType, boolean resizeAnim) { + int extraTransitType, boolean resizeAnim, int snapPosition) { super(transition, null /* consumedCallback */, null /* finishedCallback */, remoteTransition, extraTransitType); this.mResizeAnim = resizeAnim; + this.mEnteringPosition = snapPosition; } } 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 a3a808de6ff1..7472b0ea56ca 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 @@ -653,7 +653,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, null, this, isSplitScreenVisible() ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN, - !mIsDropEntering); + !mIsDropEntering, SNAP_TO_2_50_50); // Due to drag already pip task entering split by this method so need to reset flag here. mIsDropEntering = false; @@ -787,7 +787,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering, index); mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this, - extraTransitType, !mIsDropEntering); + extraTransitType, !mIsDropEntering, SNAP_TO_2_50_50); } /** @@ -833,7 +833,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering, index); mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this, - extraTransitType, !mIsDropEntering); + extraTransitType, !mIsDropEntering, SNAP_TO_2_50_50); } /** @@ -848,6 +848,27 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, "startTasks: task1=%d task2=%d position=%d snapPosition=%d", taskId1, taskId2, splitPosition, snapPosition); final WindowContainerTransaction wct = new WindowContainerTransaction(); + + // If the two tasks are already in split screen on external display, only reparent the + // split root to the default display if the app pair is clicked on default display. + // TODO(b/393217881): cover more cases and extract this to a new method when split screen + // in connected display is fully supported. + if (enableNonDefaultDisplaySplit()) { + DisplayAreaInfo displayAreaInfo = mRootTDAOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY); + ActivityManager.RunningTaskInfo taskInfo1 = mTaskOrganizer.getRunningTaskInfo(taskId1); + ActivityManager.RunningTaskInfo taskInfo2 = mTaskOrganizer.getRunningTaskInfo(taskId2); + + if (displayAreaInfo != null && taskInfo1 != null && taskInfo2 != null + && getStageOfTask(taskId1) != STAGE_TYPE_UNDEFINED + && getStageOfTask(taskId2) != STAGE_TYPE_UNDEFINED + && taskInfo1.displayId != DEFAULT_DISPLAY + && taskInfo1.displayId == taskInfo2.displayId) { + wct.reparent(mRootTaskInfo.token, displayAreaInfo.token, true); + mTaskOrganizer.applyTransaction(wct); + return; + } + } + if (taskId2 == INVALID_TASK_ID) { startSingleTask(taskId1, options1, wct, remoteTransition); return; @@ -1029,7 +1050,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mPausingTasks.clear(); } mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, remoteTransition, this, - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false, snapPosition); setEnterInstanceId(instanceId); } @@ -1119,7 +1140,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, remoteTransition, this, - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false, snapPosition); setEnterInstanceId(instanceId); } @@ -1624,6 +1645,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, grantFocusToStage(stageToFocus); } + private void grantFocusForSnapPosition(@PersistentSnapPosition int enteringPosition) { + switch (enteringPosition) { + case SNAP_TO_2_90_10 -> grantFocusToPosition(true /*leftOrTop*/); + case SNAP_TO_2_10_90 -> grantFocusToPosition(false /*leftOrTop*/); + default -> { /*no-op*/ } + } + } + private void clearRequestIfPresented() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearRequestIfPresented"); if (mSideStage.mVisible && mSideStage.mHasChildren @@ -2890,7 +2919,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // split, prepare to enter split screen. prepareEnterSplitScreen(out); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering, SNAP_TO_2_50_50); } else if (isSplitScreenVisible() && isOpening) { // launching into an existing split stage; possibly launchAdjacent // If we're replacing a pip-able app, we need to let mixed handler take care of @@ -2899,7 +2928,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // updated layout will get applied in startAnimation pendingResize mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), - TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, true /*resizeAnim*/); + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, true /*resizeAnim*/, + SNAP_TO_2_50_50); } } else if (inFullscreen && isSplitScreenVisible()) { // If the trigger task is in fullscreen and in split, exit split and place @@ -2977,14 +3007,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, out = new WindowContainerTransaction(); prepareEnterSplitScreen(out); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering, SNAP_TO_2_50_50); return out; } ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d " + "restoring to split", request.getDebugId()); out = new WindowContainerTransaction(); mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), - TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */); + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */, + SNAP_TO_2_50_50); } return out; } @@ -3171,7 +3202,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (keepSplitWithPip) { // Set an enter transition for when startAnimation gets called again mSplitTransitions.setEnterTransition(transition, /*remoteTransition*/ null, - TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false); + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false, + SNAP_TO_2_50_50); } else { int finalClosingTaskId = closingSplitTaskId; mRecentTasks.ifPresent(recentTasks -> @@ -3556,6 +3588,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }); mPausingTasks.clear(); + if (enableFlexibleTwoAppSplit()) { + grantFocusForSnapPosition(enterTransition.mEnteringPosition); + } }); if (info.getType() == TRANSIT_CHANGE && !isSplitActive() 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 ad2e23cb4028..ce786177290f 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 @@ -471,32 +471,20 @@ class HandleMenu( val rootView = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View - private val windowingButtonRippleRadius = context.resources - .getDimensionPixelSize(R.dimen.desktop_mode_handle_menu_windowing_action_ripple_radius) - private val windowingButtonDrawableInsets = DrawableInsets( - vertical = context.resources - .getDimensionPixelSize( - R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base), - horizontal = context.resources - .getDimensionPixelSize( - R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base) - ) - private val windowingButtonDrawableInsetsLeft = DrawableInsets( - vertical = context.resources - .getDimensionPixelSize( - R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base), - horizontalLeft = context.resources - .getDimensionPixelSize( - R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift), - ) - private val windowingButtonDrawableInsetsRight = DrawableInsets( - vertical = context.resources - .getDimensionPixelSize( - R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base), - horizontalRight = context.resources - .getDimensionPixelSize( - R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift) - ) + // Insets for ripple effect of App Info Pill. and Windowing Pill. buttons + val iconButtondrawableShiftInset = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_icon_button_ripple_inset_shift) + val iconButtondrawableBaseInset = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_icon_button_ripple_inset_base) + private val iconButtonRippleRadius = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_icon_button_ripple_radius) + private val iconButtonDrawableInsetsBase = DrawableInsets(t=iconButtondrawableBaseInset, + b=iconButtondrawableBaseInset, l=iconButtondrawableBaseInset, + r=iconButtondrawableBaseInset) + private val iconButtonDrawableInsetsLeft = DrawableInsets(t=iconButtondrawableBaseInset, + b=iconButtondrawableBaseInset, l=iconButtondrawableShiftInset, r=0) + private val iconButtonDrawableInsetsRight = DrawableInsets(t=iconButtondrawableBaseInset, + b=iconButtondrawableBaseInset, l=0, r=iconButtondrawableShiftInset) // App Info Pill. private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill) @@ -713,6 +701,12 @@ class HandleMenu( collapseMenuButton.apply { imageTintList = ColorStateList.valueOf(style.textColor) this.taskInfo = this@HandleMenuView.taskInfo + + background = createRippleDrawable( + color = style.textColor, + cornerRadius = iconButtonRippleRadius, + drawableInsets = iconButtonDrawableInsetsBase + ) } appNameView.setTextColor(style.textColor) appNameView.startMarquee() @@ -740,21 +734,15 @@ class HandleMenu( desktopBtn.isEnabled = !taskInfo.isFreeform desktopBtn.imageTintList = style.windowingButtonColor - val startInsets = if (context.isRtl) { - windowingButtonDrawableInsetsRight - } else { - windowingButtonDrawableInsetsLeft - } - val endInsets = if (context.isRtl) { - windowingButtonDrawableInsetsLeft - } else { - windowingButtonDrawableInsetsRight - } + val startInsets = if (context.isRtl) iconButtonDrawableInsetsRight + else iconButtonDrawableInsetsLeft + val endInsets = if (context.isRtl) iconButtonDrawableInsetsLeft + else iconButtonDrawableInsetsRight fullscreenBtn.apply { background = createRippleDrawable( color = style.textColor, - cornerRadius = windowingButtonRippleRadius, + cornerRadius = iconButtonRippleRadius, drawableInsets = startInsets ) } @@ -762,23 +750,23 @@ class HandleMenu( splitscreenBtn.apply { background = createRippleDrawable( color = style.textColor, - cornerRadius = windowingButtonRippleRadius, - drawableInsets = windowingButtonDrawableInsets + cornerRadius = iconButtonRippleRadius, + drawableInsets = iconButtonDrawableInsetsBase ) } floatingBtn.apply { background = createRippleDrawable( color = style.textColor, - cornerRadius = windowingButtonRippleRadius, - drawableInsets = windowingButtonDrawableInsets + cornerRadius = iconButtonRippleRadius, + drawableInsets = iconButtonDrawableInsetsBase ) } desktopBtn.apply { background = createRippleDrawable( color = style.textColor, - cornerRadius = windowingButtonRippleRadius, + cornerRadius = iconButtonRippleRadius, drawableInsets = endInsets ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt index bdde096d4882..e8aac39a650c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt @@ -22,7 +22,7 @@ import android.annotation.DrawableRes import android.content.Context import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.RippleDrawable +import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater import android.view.View @@ -30,11 +30,11 @@ import android.view.ViewStub import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ProgressBar +import android.window.DesktopModeFlags import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart import androidx.core.content.ContextCompat import com.android.wm.shell.R -import android.window.DesktopModeFlags private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350 private const val MAX_DRAWABLE_ALPHA = 255 @@ -109,14 +109,14 @@ class MaximizeButtonView(context: Context, attrs: AttributeSet) : FrameLayout(co darkMode: Boolean, iconForegroundColor: ColorStateList? = null, baseForegroundColor: Int? = null, - rippleDrawable: RippleDrawable? = null + backgroundDrawable: Drawable? = null ) { if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) { requireNotNull(iconForegroundColor) { "Icon foreground color must be non-null" } requireNotNull(baseForegroundColor) { "Base foreground color must be non-null" } - requireNotNull(rippleDrawable) { "Ripple drawable must be non-null" } + requireNotNull(backgroundDrawable) { "Background drawable must be non-null" } maximizeWindow.imageTintList = iconForegroundColor - maximizeWindow.background = rippleDrawable + maximizeWindow.background = backgroundDrawable stubProgressBarContainer.setOnInflateListener { _, inflated -> val progressBar = (inflated as FrameLayout) .requireViewById(R.id.progress_bar) as ProgressBar diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt index e18239d3eb70..f44b15a23b90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt @@ -16,14 +16,13 @@ package com.android.wm.shell.windowdecor.common import android.annotation.ColorInt +import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RoundRectShape -import com.android.wm.shell.windowdecor.common.OPACITY_11 -import com.android.wm.shell.windowdecor.common.OPACITY_15 -import android.content.res.ColorStateList /** * Represents drawable insets, specifying the number of pixels to inset a drawable from its bounds. @@ -52,9 +51,9 @@ fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int { * Creates a RippleDrawable with specified color, corner radius, and insets. */ fun createRippleDrawable( - @ColorInt color: Int, - cornerRadius: Int, - drawableInsets: DrawableInsets, + @ColorInt color: Int, + cornerRadius: Int, + drawableInsets: DrawableInsets, ): RippleDrawable { return RippleDrawable( ColorStateList( @@ -86,3 +85,32 @@ fun createRippleDrawable( } ) } + +/** + * Creates a background drawable with specified color, corner radius, and insets. + */ +fun createBackgroundDrawable( + @ColorInt color: Int, cornerRadius: Int, drawableInsets: DrawableInsets +): Drawable = LayerDrawable(arrayOf( + ShapeDrawable().apply { + shape = RoundRectShape( + FloatArray(8) { cornerRadius.toFloat() }, + /* inset= */ null, + /* innerRadii= */ null + ) + setTintList(ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_hovered), + intArrayOf(android.R.attr.state_pressed), + ), + intArrayOf( + replaceColorAlpha(color, OPACITY_11), + replaceColorAlpha(color, OPACITY_15), + ) + )) + } +)).apply { + require(numberOfLayers == 1) { "Must only contain one layer" } + setLayerInset(/* index= */ 0, + drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt index cb45c1732476..57f8046065b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt @@ -16,6 +16,9 @@ package com.android.wm.shell.windowdecor.tiling +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration import android.graphics.Path @@ -144,7 +147,6 @@ class DesktopTilingDividerWindowManager( * @param relativeLeash the task leash that the TilingDividerView should be shown on top of. */ fun generateViewHost(relativeLeash: SurfaceControl) { - val t = transactionSupplier.get() val surfaceControlViewHost = SurfaceControlViewHost(context, context.display, this, "DesktopTilingManager") val dividerView = @@ -155,22 +157,40 @@ class DesktopTilingDividerWindowManager( val tmpDividerBounds = Rect() getDividerBounds(tmpDividerBounds) dividerView.setup(this, tmpDividerBounds, handleRegionSize, isDarkMode) - t.setRelativeLayer(leash, relativeLeash, 1) - .setPosition( - leash, - dividerBounds.left.toFloat() - maxRoundedCornerRadius, - dividerBounds.top.toFloat(), - ) - .show(leash) - syncQueue.runInSync { transaction -> - transaction.merge(t) - t.close() - } - dividerShown = true + val dividerAnimatorT = transactionSupplier.get() + val dividerAnimator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = DIVIDER_FADE_IN_ALPHA_DURATION + addUpdateListener { + dividerAnimatorT.setAlpha(leash, animatedValue as Float).apply() + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + dividerAnimatorT + .setRelativeLayer(leash, relativeLeash, 1) + .setPosition( + leash, + dividerBounds.left.toFloat() - maxRoundedCornerRadius, + dividerBounds.top.toFloat(), + ) + .setAlpha(leash, 0f) + .show(leash) + .apply() + } + + override fun onAnimationEnd(animation: Animator) { + dividerAnimatorT.setAlpha(leash, 1f).apply() + dividerShown = true + } + } + ) + } + dividerAnimator.start() viewHost = surfaceControlViewHost - dividerView.addOnLayoutChangeListener(this) tilingDividerView = dividerView updateTouchRegion() + dividerView.addOnLayoutChangeListener(this) } /** Changes divider colour if dark/light mode is toggled. */ @@ -311,4 +331,8 @@ class DesktopTilingDividerWindowManager( ) .maxOf { position -> display.getRoundedCorner(position)?.getRadius() ?: 0 } } + + companion object { + private const val DIVIDER_FADE_IN_ALPHA_DURATION = 300L + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index a45df045041f..c3d15df6eae5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -133,10 +133,10 @@ class DesktopTilingWindowDecoration( isDarkMode = isTaskInDarkMode(taskInfo) // Observe drag resizing to break tiling if a task is drag resized. desktopModeWindowDecoration.addDragResizeListener(this) - + val callback = { initTilingForDisplayIfNeeded(taskInfo.configuration, isFirstTiledApp) } if (isTiled) { val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) - toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentBounds) + toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentBounds, callback) } else { // Handle the case where we attempt to snap resize when already snap resized: the task // position won't need to change but we want to animate the surface going back to the @@ -147,10 +147,10 @@ class DesktopTilingWindowDecoration( resizeMetadata.getLeash(), startBounds = currentBounds, endBounds = destinationBounds, + callback, ) } } - initTilingForDisplayIfNeeded(taskInfo.configuration, isFirstTiledApp) return isTiled } 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 8b054335d11c..c3f5b3f20f4a 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 @@ -23,10 +23,6 @@ import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color import android.graphics.Rect -import android.graphics.drawable.LayerDrawable -import android.graphics.drawable.RippleDrawable -import android.graphics.drawable.ShapeDrawable -import android.graphics.drawable.shapes.RoundRectShape import android.os.Bundle import android.view.View import android.view.View.OnLongClickListener @@ -55,14 +51,12 @@ import com.android.internal.R.color.materialColorSurfaceDim import com.android.wm.shell.R import com.android.wm.shell.windowdecor.MaximizeButtonView import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.common.DrawableInsets import com.android.wm.shell.windowdecor.common.OPACITY_100 -import com.android.wm.shell.windowdecor.common.OPACITY_11 -import com.android.wm.shell.windowdecor.common.OPACITY_15 import com.android.wm.shell.windowdecor.common.OPACITY_55 import com.android.wm.shell.windowdecor.common.OPACITY_65 import com.android.wm.shell.windowdecor.common.Theme -import com.android.wm.shell.windowdecor.common.DrawableInsets -import com.android.wm.shell.windowdecor.common.createRippleDrawable +import com.android.wm.shell.windowdecor.common.createBackgroundDrawable import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance @@ -385,7 +379,7 @@ class AppHeaderViewHolder( val colorStateList = ColorStateList.valueOf(foregroundColor).withAlpha(foregroundAlpha) // App chip. openMenuButton.apply { - background = createRippleDrawable( + background = createBackgroundDrawable( color = foregroundColor, cornerRadius = headerButtonsRippleRadius, drawableInsets = appChipDrawableInsets, @@ -396,11 +390,12 @@ class AppHeaderViewHolder( setTextColor(colorStateList) } appIconImageView.imageAlpha = foregroundAlpha + defaultFocusHighlightEnabled = false } // Minimize button. minimizeWindowButton.apply { imageTintList = colorStateList - background = createRippleDrawable( + background = createBackgroundDrawable( color = foregroundColor, cornerRadius = headerButtonsRippleRadius, drawableInsets = minimizeDrawableInsets @@ -413,7 +408,7 @@ class AppHeaderViewHolder( darkMode = header.appTheme == Theme.DARK, iconForegroundColor = colorStateList, baseForegroundColor = foregroundColor, - rippleDrawable = createRippleDrawable( + backgroundDrawable = createBackgroundDrawable( color = foregroundColor, cornerRadius = headerButtonsRippleRadius, drawableInsets = maximizeDrawableInsets @@ -451,7 +446,7 @@ class AppHeaderViewHolder( // Close button. closeWindowButton.apply { imageTintList = colorStateList - background = createRippleDrawable( + background = createBackgroundDrawable( color = foregroundColor, cornerRadius = headerButtonsRippleRadius, drawableInsets = closeDrawableInsets diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index d73d08c032f9..c3abe73c1d01 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.flicker import android.tools.PlatformConsts.DESKTOP_MODE_MINIMUM_WINDOW_HEIGHT import android.tools.PlatformConsts.DESKTOP_MODE_MINIMUM_WINDOW_WIDTH import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.assertors.assertions.AppLayerCoversFullScreenAtEnd import android.tools.flicker.assertors.assertions.ResizeVeilKeepsIncreasingInSize import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways @@ -99,6 +100,59 @@ class DesktopModeFlickerScenarios { .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) + val ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT = + FlickerConfigEntry( + scenarioId = ScenarioId("ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> = + transitions.filter { + it.type == TransitionType.ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd(DESKTOP_MODE_APP), + AppWindowBecomesVisible(DESKTOP_WALLPAPER) + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val EXIT_DESKTOP_FROM_KEYBOARD_SHORTCUT = + FlickerConfigEntry( + scenarioId = ScenarioId("EXIT_DESKTOP_FROM_KEYBOARD_SHORTCUT"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> = + transitions.filter { + it.type == TransitionType.EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(DESKTOP_MODE_APP), + AppLayerCoversFullScreenAtEnd(DESKTOP_MODE_APP), + AppWindowOnTopAtStart(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(DESKTOP_MODE_APP), + AppWindowBecomesInvisible(DESKTOP_WALLPAPER) + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + // Use this scenario for closing an app in desktop windowing, except the last app. For the // last app use CLOSE_LAST_APP scenario val CLOSE_APP = @@ -361,11 +415,10 @@ class DesktopModeFlickerScenarios { object : ITransitionMatcher { override fun findAll( transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { + ): Collection<Transition> = + transitions.filter { it.type == TransitionType.DESKTOP_MODE_TOGGLE_RESIZE } - } } ), assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + @@ -385,11 +438,10 @@ class DesktopModeFlickerScenarios { object : ITransitionMatcher { override fun findAll( transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { + ): Collection<Transition> = + transitions.filter { it.type == TransitionType.DESKTOP_MODE_TOGGLE_RESIZE } - } } ), assertions = @@ -410,11 +462,10 @@ class DesktopModeFlickerScenarios { object : ITransitionMatcher { override fun findAll( transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { + ): Collection<Transition> = + transitions.filter { it.type == TransitionType.TO_FRONT } - } } ), assertions = @@ -434,9 +485,8 @@ class DesktopModeFlickerScenarios { object : ITransitionMatcher { override fun findAll( transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { it.type == TransitionType.OPEN } - } + ): Collection<Transition> = + transitions.filter { it.type == TransitionType.OPEN } } ), assertions = @@ -512,9 +562,8 @@ class DesktopModeFlickerScenarios { object : ITransitionMatcher { override fun findAll( transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { it.type == TransitionType.OPEN } - } + ): Collection<Transition> = + transitions.filter { it.type == TransitionType.OPEN } } ), assertions = @@ -553,11 +602,10 @@ class DesktopModeFlickerScenarios { object : ITransitionMatcher { override fun findAll( transitions: Collection<Transition> - ): Collection<Transition> { - return listOf(transitions + ): Collection<Transition> = + listOf(transitions .filter { it.type == TransitionType.OPEN } .maxByOrNull { it.id }!!) - } } ), assertions = diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/EnterDesktopWithKeyboardShortcut.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/EnterDesktopWithKeyboardShortcut.kt new file mode 100644 index 000000000000..4603e4ed8f8e --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/EnterDesktopWithKeyboardShortcut.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.wm.shell.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT +import com.android.wm.shell.scenarios.EnterDesktopFromKeyboardShortcut +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class EnterDesktopWithKeyboardShortcut : EnterDesktopFromKeyboardShortcut() { + @ExpectedScenarios(["ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT"]) + @Test + override fun enterDesktopFromKeyboardShortcut() = super.enterDesktopFromKeyboardShortcut() + + companion object { + + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT) + .use(ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ExitDesktopToFullScreenWithKeyboardShortcut.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ExitDesktopToFullScreenWithKeyboardShortcut.kt new file mode 100644 index 000000000000..d9fd339dbf55 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ExitDesktopToFullScreenWithKeyboardShortcut.kt @@ -0,0 +1,44 @@ +/* + * 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.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.EXIT_DESKTOP_FROM_KEYBOARD_SHORTCUT +import com.android.wm.shell.scenarios.ExitDesktopFromKeyboardShortcut +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ExitDesktopToFullScreenWithKeyboardShortcut : ExitDesktopFromKeyboardShortcut() { + @ExpectedScenarios(["EXIT_DESKTOP_FROM_KEYBOARD_SHORTCUT"]) + @Test + override fun exitDesktopToFullScreenFromKeyboardShortcut() = + super.exitDesktopToFullScreenFromKeyboardShortcut() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT) + .use(EXIT_DESKTOP_FROM_KEYBOARD_SHORTCUT) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppDoubleTapAppHeaderLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppDoubleTapAppHeaderLandscape.kt new file mode 100644 index 000000000000..32df04943c83 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppDoubleTapAppHeaderLandscape.kt @@ -0,0 +1,53 @@ +/* + * 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.flicker + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper.MaximizeDesktopAppTrigger +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by double tapping on the app header. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppDoubleTapAppHeaderLandscape : MaximizeAppWindow( + rotation = ROTATION_90, + trigger = MaximizeDesktopAppTrigger.DOUBLE_TAP_APP_HEADER +) { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppDoubleTapAppHeaderPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppDoubleTapAppHeaderPortrait.kt new file mode 100644 index 000000000000..977846eac782 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppDoubleTapAppHeaderPortrait.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_0 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper.MaximizeDesktopAppTrigger +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by double tapping on the app header in portrait mode. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppDoubleTapAppHeaderPortrait : MaximizeAppWindow( + rotation = ROTATION_0, + trigger = MaximizeDesktopAppTrigger.DOUBLE_TAP_APP_HEADER +) { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppViaHeaderMenuLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppViaHeaderMenuLandscape.kt new file mode 100644 index 000000000000..70319262e1d7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppViaHeaderMenuLandscape.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper.MaximizeDesktopAppTrigger +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by tapping on the maximize button within the app header maximize menu. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppViaHeaderMenuLandscape : MaximizeAppWindow( + rotation = ROTATION_90, + trigger = MaximizeDesktopAppTrigger.MAXIMIZE_BUTTON_IN_MENU +) { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppViaHeaderMenuPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppViaHeaderMenuPortrait.kt new file mode 100644 index 000000000000..4c7eb8d210f8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppViaHeaderMenuPortrait.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_0 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper.MaximizeDesktopAppTrigger +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by tapping on the maximize button within the app header maximize menu. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppViaHeaderMenuPortrait : MaximizeAppWindow( + rotation = ROTATION_0, + trigger = MaximizeDesktopAppTrigger.MAXIMIZE_BUTTON_IN_MENU +) { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWithKeyboard.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWithKeyboard.kt index b399e9b52696..3f8a28f6e101 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWithKeyboard.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWithKeyboard.kt @@ -23,6 +23,7 @@ import android.tools.flicker.annotation.FlickerConfigProvider import android.tools.flicker.config.FlickerConfig import android.tools.flicker.config.FlickerServiceConfig import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper.MaximizeDesktopAppTrigger import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP import com.android.wm.shell.scenarios.MaximizeAppWindow import org.junit.Test @@ -35,7 +36,10 @@ import org.junit.runner.RunWith * stable display bounds. */ @RunWith(FlickerServiceJUnit4ClassRunner::class) -class MaximizeAppWithKeyboard : MaximizeAppWindow(rotation = ROTATION_90, usingKeyboard = true) { +class MaximizeAppWithKeyboard : MaximizeAppWindow( + rotation = ROTATION_90, + trigger = MaximizeDesktopAppTrigger.KEYBOARD_SHORTCUT +) { @ExpectedScenarios(["MAXIMIZE_APP"]) @Test override fun maximizeAppWindow() = super.maximizeAppWindow() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopFromKeyboardShortcut.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopFromKeyboardShortcut.kt new file mode 100644 index 000000000000..9cbc46b10ed6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopFromKeyboardShortcut.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.platform.test.annotations.Postsubmit +import android.app.Instrumentation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class EnterDesktopFromKeyboardShortcut { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val simpleAppHelper = SimpleAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(simpleAppHelper) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + } + + @Test + open fun enterDesktopFromKeyboardShortcut() { + simpleAppHelper.launchViaIntent(wmHelper) + testApp.enterDesktopModeViaKeyboard(wmHelper) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopFromKeyboardShortcut.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopFromKeyboardShortcut.kt new file mode 100644 index 000000000000..1b1f1cfca6c8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopFromKeyboardShortcut.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.platform.test.annotations.Postsubmit +import android.app.Instrumentation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class ExitDesktopFromKeyboardShortcut { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val simpleAppHelper = SimpleAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(simpleAppHelper) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + simpleAppHelper.launchViaIntent(wmHelper) + testApp.enterDesktopMode(wmHelper, device) + } + + @Test + open fun exitDesktopToFullScreenFromKeyboardShortcut() { + testApp.exitDesktopModeToFullScreenViaKeyboard(wmHelper) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt index 7855698d0151..92fe40d96fd7 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt @@ -26,6 +26,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper.MaximizeDesktopAppTrigger import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags @@ -38,11 +39,10 @@ import org.junit.Rule import org.junit.Test @Ignore("Test Base Class") -abstract class MaximizeAppWindow -constructor( +abstract class MaximizeAppWindow( private val rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true, - private val usingKeyboard: Boolean = false + private val trigger: MaximizeDesktopAppTrigger = MaximizeDesktopAppTrigger.MAXIMIZE_MENU, ) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() @@ -59,7 +59,7 @@ constructor( @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - if (usingKeyboard) { + if (trigger == MaximizeDesktopAppTrigger.KEYBOARD_SHORTCUT) { Assume.assumeTrue(DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue) } tapl.setEnableRotation(true) @@ -70,7 +70,7 @@ constructor( @Test open fun maximizeAppWindow() { - testApp.maximiseDesktopApp(wmHelper, device, usingKeyboard = usingKeyboard) + testApp.maximiseDesktopApp(wmHelper, device, trigger) } @After diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt index 6a6aa1abc9f3..fa9864b539ee 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt @@ -17,15 +17,15 @@ package com.android.wm.shell.flicker.splitscreen.benchmark import android.tools.NavBar -import android.tools.Rotation import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory -import android.tools.helpers.WindowUtils import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.UiDevice import com.android.wm.shell.flicker.utils.SplitScreenUtils +import com.android.wm.shell.flicker.utils.SplitScreenUtils.isLeftRightSplit import org.junit.FixMethodOrder import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -37,6 +37,8 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) abstract class SwitchAppByDoubleTapDividerBenchmark(override val flicker: LegacyFlickerTest) : SplitScreenBase(flicker) { + private val device = UiDevice.getInstance(instrumentation) + protected val thisTransition: FlickerBuilder.() -> Unit get() = { setup { @@ -73,7 +75,8 @@ abstract class SwitchAppByDoubleTapDividerBenchmark(override val flicker: Legacy } ?: return@add false - if (isLandscape(flicker.scenario.endRotation)) { + if (isLeftRightSplit(instrumentation.context, flicker.scenario.endRotation, + device.displaySizeDp)) { return@add if (flicker.scenario.isTablet) { secondaryAppWindow.frame.right <= primaryAppWindow.frame.left } else { @@ -109,7 +112,8 @@ abstract class SwitchAppByDoubleTapDividerBenchmark(override val flicker: Legacy val secondaryVisibleRegion = secondaryAppLayer.visibleRegion?.bounds ?: return@add false - if (isLandscape(flicker.scenario.endRotation)) { + if (isLeftRightSplit(instrumentation.context, flicker.scenario.endRotation, + device.displaySizeDp)) { return@add if (flicker.scenario.isTablet) { secondaryVisibleRegion.right <= primaryVisibleRegion.left } else { @@ -126,11 +130,6 @@ abstract class SwitchAppByDoubleTapDividerBenchmark(override val flicker: Legacy .waitForAndVerify() } - private fun isLandscape(rotation: Rotation): Boolean { - val displayBounds = WindowUtils.getDisplayBounds(rotation) - return displayBounds.width() > displayBounds.height() - } - companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt index 26203d4afccd..3fd93d3eaf59 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/SwitchAppByDoubleTapDivider.kt @@ -17,16 +17,16 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation -import android.graphics.Point import android.tools.NavBar import android.tools.Rotation -import android.tools.helpers.WindowUtils import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils import com.android.wm.shell.flicker.utils.SplitScreenUtils +import com.android.wm.shell.flicker.utils.SplitScreenUtils.isLeftRightSplit +import com.android.wm.shell.flicker.utils.SplitScreenUtils.isTablet import org.junit.After import org.junit.Before import org.junit.Ignore @@ -89,14 +89,14 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { } ?: return@add false - if (isLandscape(rotation)) { - return@add if (isTablet()) { + if (isLeftRightSplit(instrumentation.context, rotation, device.displaySizeDp)) { + return@add if (isTablet(device.displaySizeDp)) { secondaryAppWindow.frame.right <= primaryAppWindow.frame.left } else { primaryAppWindow.frame.right <= secondaryAppWindow.frame.left } } else { - return@add if (isTablet()) { + return@add if (isTablet(device.displaySizeDp)) { primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top } else { primaryAppWindow.frame.bottom <= secondaryAppWindow.frame.top @@ -125,14 +125,14 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { val secondaryVisibleRegion = secondaryAppLayer.visibleRegion?.bounds ?: return@add false - if (isLandscape(rotation)) { - return@add if (isTablet()) { + if (isLeftRightSplit(instrumentation.context, rotation, device.displaySizeDp)) { + return@add if (isTablet(device.displaySizeDp)) { secondaryVisibleRegion.right <= primaryVisibleRegion.left } else { primaryVisibleRegion.right <= secondaryVisibleRegion.left } } else { - return@add if (isTablet()) { + return@add if (isTablet(device.displaySizeDp)) { primaryVisibleRegion.bottom <= secondaryVisibleRegion.top } else { primaryVisibleRegion.bottom <= secondaryVisibleRegion.top @@ -141,15 +141,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { } .waitForAndVerify() } - - private fun isLandscape(rotation: Rotation): Boolean { - val displayBounds = WindowUtils.getDisplayBounds(rotation) - return displayBounds.width() > displayBounds.height() - } - - private fun isTablet(): Boolean { - val sizeDp: Point = device.displaySizeDp - val LARGE_SCREEN_DP_THRESHOLD = 600 - return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD - } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt index feb3edc9dab7..49d6877a1654 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt @@ -17,12 +17,14 @@ package com.android.wm.shell.flicker.utils import android.app.Instrumentation +import android.content.Context import android.graphics.Point import android.os.SystemClock import android.tools.Rotation import android.tools.device.apphelpers.IStandardAppHelper import android.tools.device.apphelpers.StandardAppHelper import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.helpers.WindowUtils import android.tools.traces.component.ComponentNameMatcher import android.tools.traces.component.IComponentMatcher import android.tools.traces.component.IComponentNameMatcher @@ -393,4 +395,24 @@ object SplitScreenUtils { error("Fail to copy content in split") } } + + fun isLeftRightSplit(context: Context, rotation: Rotation, displaySizeDp: Point): Boolean { + val allowLeftRightSplit = context.resources.getBoolean( + com.android.internal.R.bool.config_leftRightSplitInPortrait) + val displayBounds = WindowUtils.getDisplayBounds(rotation) + val isLandscape = displayBounds.width() > displayBounds.height() + if (allowLeftRightSplit && isTablet(displaySizeDp)) { + // Certain devices allow left/right split in portrait, so they end up with top/bottom + // split in landscape + return !isLandscape + } else { + return isLandscape + } + } + + fun isTablet(displaySizeDp: Point): Boolean { + val LARGE_SCREEN_DP_THRESHOLD = 600 + return displaySizeDp.x >= LARGE_SCREEN_DP_THRESHOLD + && displaySizeDp.y >= LARGE_SCREEN_DP_THRESHOLD + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java index 42310caba1c6..3c79ea7be39f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -25,6 +25,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -35,6 +36,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.graphics.PointF; import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; @@ -138,13 +140,14 @@ public class BubbleTransitionsTest extends ShellTestCase { return taskInfo; } - private TransitionInfo setupFullscreenTaskTransition(ActivityManager.RunningTaskInfo taskInfo) { + private TransitionInfo setupFullscreenTaskTransition(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl taskLeash, SurfaceControl snapshot) { final TransitionInfo info = new TransitionInfo(TRANSIT_CONVERT_TO_BUBBLE, 0); - final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token, - mock(SurfaceControl.class)); + final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token, taskLeash); chg.setTaskInfo(taskInfo); chg.setMode(TRANSIT_CHANGE); chg.setStartAbsBounds(new Rect(0, 0, FULLSCREEN_TASK_WIDTH, FULLSCREEN_TASK_HEIGHT)); + chg.setSnapshot(snapshot, /* luma= */ 0f); info.addChange(chg); info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); return info; @@ -172,7 +175,9 @@ public class BubbleTransitionsTest extends ShellTestCase { // Ensure we are communicating with the taskviewtransitions queue assertTrue(mTaskViewTransitions.hasPending()); - final TransitionInfo info = setupFullscreenTaskTransition(taskInfo); + SurfaceControl taskLeash = new SurfaceControl.Builder().setName("taskLeash").build(); + SurfaceControl snapshot = new SurfaceControl.Builder().setName("snapshot").build(); + final TransitionInfo info = setupFullscreenTaskTransition(taskInfo, taskLeash, snapshot); SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); final boolean[] finishCalled = new boolean[]{false}; @@ -183,7 +188,8 @@ public class BubbleTransitionsTest extends ShellTestCase { ctb.startAnimation(ctb.mTransition, info, startT, finishT, finishCb); assertFalse(mTaskViewTransitions.hasPending()); - verify(startT).setPosition(any(), eq(0f), eq(0f)); + verify(startT).setPosition(taskLeash, 0, 0); + verify(startT).setPosition(snapshot, 0, 0); verify(mBubbleData).notificationEntryUpdated(eq(mBubble), anyBoolean(), anyBoolean()); @@ -194,7 +200,7 @@ public class BubbleTransitionsTest extends ShellTestCase { // Check that preparing transition is not reset before continueExpand is called verify(mBubble, never()).setPreparingTransition(any()); ArgumentCaptor<Runnable> animCb = ArgumentCaptor.forClass(Runnable.class); - verify(mLayerView).animateConvert(any(), any(), any(), any(), animCb.capture()); + verify(mLayerView).animateConvert(any(), any(), anyFloat(), any(), any(), animCb.capture()); // continueExpand is now called, check that preparing transition is cleared ctb.continueExpand(); @@ -209,14 +215,14 @@ public class BubbleTransitionsTest extends ShellTestCase { public void testConvertToBubble_drag() { ActivityManager.RunningTaskInfo taskInfo = setupBubble(); - Rect draggedTaskBounds = new Rect(10, 20, 30, 40); WindowContainerTransaction pendingWct = new WindowContainerTransaction(); WindowContainerToken pendingDragOpToken = createMockToken(); pendingWct.reorder(pendingDragOpToken, /* onTop= */ false); + PointF dragPosition = new PointF(10f, 20f); BubbleTransitions.DragData dragData = new BubbleTransitions.DragData( - draggedTaskBounds, pendingWct, /* releasedOnLeft= */ false - ); + /* releasedOnLeft= */ false, /* taskScale= */ 0.5f, dragPosition, + pendingWct); final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble( mBubble, taskInfo, mExpandedViewManager, mTaskViewFactory, mBubblePositioner, @@ -234,15 +240,19 @@ public class BubbleTransitionsTest extends ShellTestCase { == WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER && op.getContainer() == pendingDragOpToken.asBinder())).isTrue(); - final TransitionInfo info = setupFullscreenTaskTransition(taskInfo); + SurfaceControl taskLeash = new SurfaceControl.Builder().setName("taskLeash").build(); + SurfaceControl snapshot = new SurfaceControl.Builder().setName("snapshot").build(); + final TransitionInfo info = setupFullscreenTaskTransition(taskInfo, taskLeash, snapshot); SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); Transitions.TransitionFinishCallback finishCb = wct -> {}; ctb.startAnimation(ctb.mTransition, info, startT, finishT, finishCb); - // Verify that dragged task bounds are used for the position - verify(startT).setPosition(any(), eq((float) draggedTaskBounds.left), - eq((float) draggedTaskBounds.top)); + // Verify that snapshot and task are placed at where the drag ended + verify(startT).setPosition(taskLeash, dragPosition.x, dragPosition.y); + verify(startT).setPosition(snapshot, dragPosition.x, dragPosition.y); + // Snapshot has the scale of the dragged task + verify(startT).setScale(snapshot, dragData.getTaskScale(), dragData.getTaskScale()); } @Test 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 70a30a3ca7a9..d58f8a34c98e 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 @@ -188,7 +188,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { SCREEN_ORIENTATION_LANDSCAPE, ) - verify(resizeTransitionHandler, never()).startTransition(any(), any()) + verify(resizeTransitionHandler, never()).startTransition(any(), any(), any()) } @Test @@ -209,7 +209,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { SCREEN_ORIENTATION_LANDSCAPE, ) - verify(resizeTransitionHandler, never()).startTransition(any(), any()) + verify(resizeTransitionHandler, never()).startTransition(any(), any(), any()) } @Test @@ -225,7 +225,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { handler.handleActivityOrientationChange(task, newTask) - verify(resizeTransitionHandler, never()).startTransition(any(), any()) + verify(resizeTransitionHandler, never()).startTransition(any(), any(), any()) } @Test @@ -240,7 +240,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { SCREEN_ORIENTATION_LANDSCAPE, ) - verify(resizeTransitionHandler, never()).startTransition(any(), any()) + verify(resizeTransitionHandler, never()).startTransition(any(), any(), any()) } @Test @@ -318,7 +318,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { val arg: ArgumentCaptor<WindowContainerTransaction> = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) verify(resizeTransitionHandler, atLeastOnce()) - .startTransition(capture(arg), eq(currentBounds)) + .startTransition(capture(arg), eq(currentBounds), isNull()) return arg.value } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt new file mode 100644 index 000000000000..fbc940663d19 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt @@ -0,0 +1,87 @@ +/* + * 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 + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.WindowManager +import android.window.TransitionInfo +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.util.StubTransaction +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() { + private lateinit var handler: DesktopModeMoveToDisplayTransitionHandler + + @Before + fun setUp() { + handler = DesktopModeMoveToDisplayTransitionHandler(StubTransaction()) + } + + @Test + fun handleRequest_returnsNull() { + assertNull(handler.handleRequest(mock(), mock())) + } + + @Test + fun startAnimation_changeWithinDisplay_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = + TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply { + addChange( + TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 1) } + ) + }, + startTransaction = StubTransaction(), + finishTransaction = StubTransaction(), + finishCallback = mock(), + ) + + assertFalse("Should not animate open transition", animates) + } + + @Test + fun startAnimation_changeMoveToDisplay_returnsTrue() { + val animates = + handler.startAnimation( + transition = mock(), + info = + TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply { + addChange( + TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 2) } + ) + }, + startTransaction = StubTransaction(), + finishTransaction = StubTransaction(), + finishCallback = mock(), + ) + + assertTrue("Should animate display change transition", animates) + } +} 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 dcc9e2415039..fe1dc29181b9 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 @@ -20,6 +20,7 @@ import android.animation.AnimatorTestRule import android.app.ActivityManager.RunningTaskInfo import android.graphics.PointF import android.graphics.Rect +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -28,6 +29,7 @@ import android.view.SurfaceControl import androidx.test.filters.SmallTest import com.android.internal.policy.SystemBarUtils import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase @@ -45,6 +47,8 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever /** @@ -345,6 +349,38 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { assertThat(visualIndicator.indicatorBounds).isEqualTo(dropTargetBounds) } + @Test + @DisableFlags(FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX) + fun createIndicator_inTransitionFlagDisabled_isAttachedToDisplayArea() { + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + + verify(taskDisplayAreaOrganizer).attachToDisplayArea(anyInt(), any()) + } + + @Test + @EnableFlags(FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX) + fun createIndicator_fromFreeform_inTransitionFlagEnabled_isAttachedToDisplayArea() { + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) + + verify(taskDisplayAreaOrganizer).attachToDisplayArea(anyInt(), any()) + } + + @Test + @EnableFlags(FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX) + fun createIndicator_fromFullscreen_inTransitionFlagEnabled_notAttachedToDisplayArea() { + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + + verify(taskDisplayAreaOrganizer, never()).attachToDisplayArea(anyInt(), any()) + } + + @Test + @EnableFlags(FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX) + fun createIndicator_fromSplit_inTransitionFlagEnabled_notAttachedToDisplayArea() { + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + + verify(taskDisplayAreaOrganizer, never()).attachToDisplayArea(anyInt(), any()) + } + private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) { visualIndicator = DesktopModeVisualIndicator( 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 6b0ee5b7ffd4..54360a8fd908 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 @@ -180,6 +180,28 @@ class DesktopTaskChangeListenerTest : ShellTestCase() { } @Test + fun onTaskMovingToBack_activeTaskInRepo_updatesTask() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskMovingToBack(task) + + verify(desktopUserRepositories.current) + .updateTask(task.displayId, task.taskId, /* isVisible= */ false) + } + + @Test + fun onTaskMovingToBack_nonActiveTaskInRepo_noop() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(false) + + desktopTaskChangeListener.onTaskMovingToBack(task) + + verify(desktopUserRepositories.current, never()) + .updateTask(task.displayId, task.taskId, /* isVisible= */ false) + } + + @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun onTaskClosing_backNavEnabled_nonClosingTask_minimizesTaskInRepo() { val task = createFreeformTask().apply { isVisible = true } 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 0bccde13cabd..8f499c959759 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 @@ -263,6 +263,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var mockDisplayContext: Context @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler + @Mock + private lateinit var moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit @@ -445,6 +447,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() userProfileContexts, desktopModeCompatPolicy, dragToDisplayTransitionHandler, + moveToDisplayTransitionHandler, ) @After @@ -2521,7 +2524,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) controller.moveToNextDisplay(task.taskId) - verifyWCTNotExecuted() + verify(transitions, never()).startTransition(anyInt(), any(), anyOrNull()) } @Test @@ -2539,9 +2542,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) val taskChange = - getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { - it.container == task.token.asBinder() && it.isReparent - } + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .hierarchyOps + .find { it.container == task.token.asBinder() && it.isReparent } assertNotNull(taskChange) assertThat(taskChange.newParent).isEqualTo(secondDisplayArea.token.asBinder()) assertThat(taskChange.toTop).isTrue() @@ -2562,9 +2568,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) val taskChange = - getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { - it.container == task.token.asBinder() && it.isReparent - } + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .hierarchyOps + .find { it.container == task.token.asBinder() && it.isReparent } assertNotNull(taskChange) assertThat(taskChange.newParent).isEqualTo(defaultDisplayArea.token.asBinder()) assertThat(taskChange.toTop).isTrue() @@ -2589,7 +2598,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) - with(getLatestWct(type = TRANSIT_CHANGE)) { + with( + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + ) { val wallpaperChange = hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() } assertNotNull(wallpaperChange) @@ -2615,9 +2629,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) val wallpaperChange = - getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { op -> - op.container == wallpaperToken.asBinder() - } + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .hierarchyOps + .find { op -> op.container == wallpaperToken.asBinder() } assertNotNull(wallpaperChange) assertThat(wallpaperChange.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) } @@ -2649,7 +2666,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) - val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()] + val taskChange = + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .changes[task.token.asBinder()] assertNotNull(taskChange) // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin // to the right margin and the ratio of the top margin to bottom margin are also @@ -2686,7 +2708,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) - val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()] + val taskChange = + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .changes[task.token.asBinder()] assertNotNull(taskChange) assertThat(taskChange.configuration.windowConfiguration.bounds) .isEqualTo(Rect(960, 480, 1280, 720)) @@ -2717,7 +2744,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) - val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()] + val taskChange = + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .changes[task.token.asBinder()] assertNotNull(taskChange) // DP size is preserved. The window is centered in the destination display. assertThat(taskChange.configuration.windowConfiguration.bounds) @@ -2755,7 +2787,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) - val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()] + val taskChange = + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .changes[task.token.asBinder()] assertNotNull(taskChange) assertThat(taskChange.configuration.windowConfiguration.bounds.left).isAtLeast(0) assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0) @@ -2782,9 +2819,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.moveToNextDisplay(task.taskId) val taskChange = - getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { - it.container == task.token.asBinder() && it.type == HIERARCHY_OP_TYPE_REORDER - } + getLatestWct( + type = TRANSIT_CHANGE, + handlerClass = DesktopModeMoveToDisplayTransitionHandler::class.java, + ) + .hierarchyOps + .find { + it.container == task.token.asBinder() && it.type == HIERARCHY_OP_TYPE_REORDER + } assertNotNull(taskChange) assertThat(taskChange.toTop).isTrue() assertThat(taskChange.includingParents()).isTrue() @@ -2885,11 +2927,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desksOrganizer).activateDesk(any(), eq(targetDeskId)) verify(desksTransitionsObserver) .addPendingTransition( - DeskTransition.ActiveDeskWithTask( + DeskTransition.ActivateDesk( token = transition, displayId = SECOND_DISPLAY, deskId = targetDeskId, - enterTaskId = task.taskId, ) ) } @@ -3525,9 +3566,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) // Make sure we reorder the new task to top, and the back task to the bottom - assertThat(wct!!.hierarchyOps.size).isEqualTo(9) + assertThat(wct!!.hierarchyOps.size).isEqualTo(8) wct.assertReorderAt(0, fullscreenTask, toTop = true) - wct.assertReorderAt(8, freeformTasks[0], toTop = false) + // Oldest task that needs to minimized is never reordered to top over Home. + val taskToMinimize = freeformTasks[0] + wct.assertWithoutHop { hop -> + hop.container == taskToMinimize.token && + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.toTop == true + } } @Test @@ -3542,12 +3589,18 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - assertThat(wct!!.hierarchyOps.size).isEqualTo(10) + assertThat(wct!!.hierarchyOps.size).isEqualTo(9) wct.assertReorderAt(0, fullscreenTask, toTop = true) // Make sure we reorder the home task to the top, desktop tasks to top of them and minimized // task is under the home task. wct.assertReorderAt(1, homeTask, toTop = true) - wct.assertReorderAt(9, freeformTasks[0], toTop = false) + // Oldest task that needs to minimized is never reordered to top over Home. + val taskToMinimize = freeformTasks[0] + wct.assertWithoutHop { hop -> + hop.container == taskToMinimize.token && + hop.type == HIERARCHY_OP_TYPE_REORDER && + hop.toTop == true + } } @Test @@ -5253,7 +5306,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) // Assert that task is NOT updated via WCT - verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + verify(toggleResizeDesktopTaskTransitionHandler, never()) + .startTransition(any(), any(), any()) // Assert that task leash is updated via Surface Animations verify(mReturnToDragStartAnimator) .start( @@ -5738,7 +5792,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() InputMethod.TOUCH, ) // Assert that task is NOT updated via WCT - verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + verify(toggleResizeDesktopTaskTransitionHandler, never()) + .startTransition(any(), any(), any()) // Assert that task leash is updated via Surface Animations verify(mReturnToDragStartAnimator) @@ -5835,7 +5890,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ) // Assert that task is NOT updated via WCT - verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + verify(toggleResizeDesktopTaskTransitionHandler, never()) + .startTransition(any(), any(), any()) verify(mockToast).show() } @@ -6903,7 +6959,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ): WindowContainerTransaction { val arg = argumentCaptor<WindowContainerTransaction>() verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) - .startTransition(arg.capture(), eq(currentBounds)) + .startTransition(arg.capture(), eq(currentBounds), isNull()) return arg.lastValue } 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 62e3c544e390..76103640c029 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 @@ -172,6 +172,20 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test + fun createDesktopTasksLimiter_withNoLimit_shouldSucceed() { + // Instantiation should succeed without an error. + DesktopTasksLimiter( + transitions, + userRepositories, + shellTaskOrganizer, + maxTasksLimit = null, + interactionJankMonitor, + mContext, + handler, + ) + } + + @Test fun addPendingMinimizeTransition_taskIsNotMinimized() { desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) 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 de55db86d1e7..0871d38ceb46 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 @@ -11,6 +11,8 @@ import android.graphics.PointF import android.graphics.Rect import android.os.IBinder import android.os.SystemProperties +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl @@ -23,6 +25,7 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE import com.android.internal.jank.InteractionJankMonitor +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder @@ -78,6 +81,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var homeTaskLeash: SurfaceControl @Mock private lateinit var desktopUserRepositories: DesktopUserRepositories @Mock private lateinit var bubbleController: BubbleController + @Mock private lateinit var visualIndicator: DesktopModeVisualIndicator private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } @@ -740,11 +744,47 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { assertThat(fraction).isWithin(TOLERANCE).of(0f) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX) + fun startDrag_indicatorFlagEnabled_attachesIndicatorToTransitionRoot() { + val task = createTask() + val rootLeash = mock<SurfaceControl>() + val startTransaction = mock<SurfaceControl.Transaction>() + startDrag( + defaultHandler, + task, + startTransaction = startTransaction, + transitionRootLeash = rootLeash, + ) + + verify(visualIndicator).reparentLeash(startTransaction, rootLeash) + verify(visualIndicator).fadeInIndicator() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_VISUAL_INDICATOR_IN_TRANSITION_BUGFIX) + fun startDrag_indicatorFlagDisabled_doesNotAttachIndicatorToTransitionRoot() { + val task = createTask() + val rootLeash = mock<SurfaceControl>() + val startTransaction = mock<SurfaceControl.Transaction>() + startDrag( + defaultHandler, + task, + startTransaction = startTransaction, + transitionRootLeash = rootLeash, + ) + + verify(visualIndicator, never()).reparentLeash(any(), any()) + verify(visualIndicator, never()).fadeInIndicator() + } + private fun startDrag( handler: DragToDesktopTransitionHandler, task: RunningTaskInfo = createTask(), + startTransaction: SurfaceControl.Transaction = mock(), finishTransaction: SurfaceControl.Transaction = mock(), homeChange: TransitionInfo.Change? = createHomeChange(), + transitionRootLeash: SurfaceControl = mock(), ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. @@ -756,8 +796,9 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, draggedTask = task, homeChange = homeChange, + rootLeash = transitionRootLeash, ), - startTransaction = mock(), + startTransaction = startTransaction, finishTransaction = finishTransaction, finishCallback = {}, ) @@ -778,7 +819,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) ) .thenReturn(token) - handler.startDragToDesktopTransition(task, dragAnimator) + handler.startDragToDesktopTransition(task, dragAnimator, visualIndicator) return token } @@ -845,6 +886,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { type: Int, draggedTask: RunningTaskInfo, homeChange: TransitionInfo.Change? = createHomeChange(), + rootLeash: SurfaceControl = mock(), ) = TransitionInfo(type, /* flags= */ 0).apply { homeChange?.let { addChange(it) } @@ -861,6 +903,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { flags = flags or FLAG_IS_WALLPAPER } ) + addRootLeash(draggedTask.displayId, rootLeash, /* offsetLeft= */ 0, /* offsetTop= */ 0) } private fun createHomeChange() = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt index 4c8cb3823f7e..c7518d5914b4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt @@ -25,6 +25,7 @@ import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.Display +import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View @@ -49,6 +50,8 @@ import org.mockito.Mockito.mock import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.verifyZeroInteractions @@ -121,7 +124,7 @@ class VisualIndicatorViewContainerTest : ShellTestCase() { DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, ) desktopExecutor.flushAll() - verify(spyViewContainer).fadeInIndicator(any(), any(), any(), any()) + verify(spyViewContainer).fadeInIndicatorInternal(any(), any(), any(), any()) } @Test @@ -265,6 +268,35 @@ class VisualIndicatorViewContainerTest : ShellTestCase() { ) } + @Test + fun fadeInIndicator_callsFadeIn() { + val spyViewContainer = setupSpyViewContainer() + + spyViewContainer.fadeInIndicator( + mock<DisplayLayout>(), + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DEFAULT_DISPLAY, + ) + desktopExecutor.flushAll() + + verify(spyViewContainer).fadeInIndicatorInternal(any(), any(), any(), any()) + } + + @Test + fun fadeInIndicator_alreadyReleased_doesntCallFadeIn() { + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.releaseVisualIndicator() + + spyViewContainer.fadeInIndicator( + mock<DisplayLayout>(), + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DEFAULT_DISPLAY, + ) + desktopExecutor.flushAll() + + verify(spyViewContainer, never()).fadeInIndicatorInternal(any(), any(), any(), any()) + } + private fun setupSpyViewContainer(): VisualIndicatorViewContainer { val viewContainer = VisualIndicatorViewContainer( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index e5a6a6d258dd..70603fad37b9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -27,6 +27,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; @@ -213,7 +214,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mSplitScreenTransitions.startEnterTransition( TRANSIT_OPEN, new WindowContainerTransaction(), new RemoteTransition(testRemote, "Test"), mStageCoordinator, - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false, SNAP_TO_2_50_50); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -239,7 +240,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mSplitScreenTransitions.startEnterTransition( TRANSIT_OPEN, new WindowContainerTransaction(), new RemoteTransition(testRemote, "Test"), mStageCoordinator, - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false, SNAP_TO_2_50_50); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), @@ -262,7 +263,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mSplitScreenTransitions.startEnterTransition( TRANSIT_OPEN, new WindowContainerTransaction(), new RemoteTransition(testRemote, "Test"), mStageCoordinator, - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false, SNAP_TO_2_50_50); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), @@ -524,7 +525,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder enterTransit = mSplitScreenTransitions.startEnterTransition( TRANSIT_OPEN, new WindowContainerTransaction(), new RemoteTransition(new TestRemoteTransition(), "Test"), - mStageCoordinator, TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false); + mStageCoordinator, TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false, SNAP_TO_2_50_50); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); mStageCoordinator.startAnimation(enterTransit, enterInfo, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index e9c4c31729e9..e246329446dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -475,7 +475,7 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator.startTask(mTaskId, SPLIT_POSITION_TOP_OR_LEFT, null /*options*/, null, SPLIT_INDEX_UNDEFINED); verify(mSplitScreenTransitions).startEnterTransition(anyInt(), - mWctCaptor.capture(), any(), any(), anyInt(), anyBoolean()); + mWctCaptor.capture(), any(), any(), anyInt(), anyBoolean(), anyInt()); int windowingMode = mWctCaptor.getValue().getChanges().get(mBinder).getWindowingMode(); assertEquals(windowingMode, WINDOWING_MODE_UNDEFINED); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt index 844205682d31..42eab14042f9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt @@ -70,6 +70,13 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { whenever(context.display).thenReturn(display) whenever(display.getRoundedCorner(any())).thenReturn(roundedCorner) whenever(roundedCorner.radius).thenReturn(CORNER_RADIUS) + whenever(transactionSupplierMock.get()).thenReturn(transaction) + whenever(transaction.show(any())).thenReturn(transaction) + whenever(transaction.setAlpha(any(), any())).thenReturn(transaction) + whenever(transaction.hide(any())).thenReturn(transaction) + whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction) + whenever(transaction.remove(any())).thenReturn(transaction) desktopTilingWindowManager = DesktopTilingDividerWindowManager( config, @@ -88,12 +95,6 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { @Test @UiThreadTest fun testWindowManager_isInitialisedAndReleased() { - whenever(transactionSupplierMock.get()).thenReturn(transaction) - whenever(transaction.hide(any())).thenReturn(transaction) - whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction) - whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction) - whenever(transaction.remove(any())).thenReturn(transaction) - desktopTilingWindowManager.generateViewHost(surfaceControl) // Ensure a surfaceControl transaction runs to show the divider. @@ -102,18 +103,11 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { desktopTilingWindowManager.release() verify(transaction, times(1)).hide(any()) verify(transaction, times(1)).remove(any()) - verify(transaction, times(1)).apply() } @Test @UiThreadTest fun testWindowManager_accountsForRoundedCornerDimensions() { - whenever(transactionSupplierMock.get()).thenReturn(transaction) - whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction) - whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction) - whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction) - whenever(transaction.show(any())).thenReturn(transaction) - desktopTilingWindowManager.generateViewHost(surfaceControl) // Ensure a surfaceControl transaction runs to show the divider. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt index 399a51e1ed08..bc8faedd77a9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt @@ -114,6 +114,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { private val split_divider_width = 10 @Captor private lateinit var wctCaptor: ArgumentCaptor<WindowContainerTransaction> + @Captor private lateinit var callbackCaptor: ArgumentCaptor<(() -> Unit)> @Before fun setUp() { @@ -134,7 +135,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { userRepositories, desktopModeEventLogger, focusTransitionObserver, - mainExecutor + mainExecutor, ) whenever(context.createContextAsUser(any(), any())).thenReturn(context) whenever(userRepositories.current).thenReturn(desktopRepository) @@ -158,7 +159,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { BOUNDS, ) - verify(toggleResizeDesktopTaskTransitionHandler).startTransition(capture(wctCaptor), any()) + verify(toggleResizeDesktopTaskTransitionHandler) + .startTransition(capture(wctCaptor), any(), any()) for (change in wctCaptor.value.changes) { val bounds = change.value.configuration.windowConfiguration.bounds val leftBounds = getLeftTaskBounds() @@ -185,7 +187,8 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { BOUNDS, ) - verify(toggleResizeDesktopTaskTransitionHandler).startTransition(capture(wctCaptor), any()) + verify(toggleResizeDesktopTaskTransitionHandler) + .startTransition(capture(wctCaptor), any(), any()) for (change in wctCaptor.value.changes) { val bounds = change.value.configuration.windowConfiguration.bounds val leftBounds = getRightTaskBounds() @@ -220,7 +223,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { ) verify(toggleResizeDesktopTaskTransitionHandler, times(1)) - .startTransition(capture(wctCaptor), any()) + .startTransition(capture(wctCaptor), any(), any()) verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), anyOrNull()) for (change in wctCaptor.value.changes) { val bounds = change.value.configuration.windowConfiguration.bounds @@ -308,9 +311,13 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { DesktopTasksController.SnapPosition.LEFT, BOUNDS, ) + verify(toggleResizeDesktopTaskTransitionHandler, times(2)) + .startTransition(capture(wctCaptor), any(), capture(callbackCaptor)) + (callbackCaptor.value).invoke() task1.isFocused = true - assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, isFocusedOnDisplay = true)).isTrue() + assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, isFocusedOnDisplay = true)) + .isTrue() verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null)) } @@ -341,6 +348,9 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { ) task1.isFocused = true task3.isFocused = true + verify(toggleResizeDesktopTaskTransitionHandler, times(2)) + .startTransition(capture(wctCaptor), any(), capture(callbackCaptor)) + (callbackCaptor.value).invoke() assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, true)).isFalse() assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, true)).isTrue() @@ -372,9 +382,14 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { DesktopTasksController.SnapPosition.LEFT, BOUNDS, ) - - assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, isFocusedOnDisplay = true)).isFalse() - assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, isFocusedOnDisplay = true)).isTrue() + verify(toggleResizeDesktopTaskTransitionHandler, times(2)) + .startTransition(capture(wctCaptor), any(), capture(callbackCaptor)) + (callbackCaptor.value).invoke() + + assertThat(tilingDecoration.moveTiledPairToFront(task3.taskId, isFocusedOnDisplay = true)) + .isFalse() + assertThat(tilingDecoration.moveTiledPairToFront(task1.taskId, isFocusedOnDisplay = true)) + .isTrue() verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null)) } @@ -482,27 +497,29 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { tilingDecoration.onDividerHandleDragStart(motionEvent) // Log start event for task1 and task2, but the tasks are the same in // this test, so we verify the same log twice. - verify(desktopModeEventLogger, times(2)).logTaskResizingStarted( - ResizeTrigger.TILING_DIVIDER, - DesktopModeEventLogger.Companion.InputMethod.UNKNOWN_INPUT_METHOD, - task1, - BOUNDS.width() / 2, - BOUNDS.height(), - displayController, - ) + verify(desktopModeEventLogger, times(2)) + .logTaskResizingStarted( + ResizeTrigger.TILING_DIVIDER, + DesktopModeEventLogger.Companion.InputMethod.UNKNOWN_INPUT_METHOD, + task1, + BOUNDS.width() / 2, + BOUNDS.height(), + displayController, + ) tilingDecoration.onDividerHandleMoved(BOUNDS, transaction) tilingDecoration.onDividerHandleDragEnd(BOUNDS, transaction, motionEvent) // Log end event for task1 and task2, but the tasks are the same in // this test, so we verify the same log twice. - verify(desktopModeEventLogger, times(2)).logTaskResizingEnded( - ResizeTrigger.TILING_DIVIDER, - DesktopModeEventLogger.Companion.InputMethod.UNKNOWN_INPUT_METHOD, - task1, - BOUNDS.width(), - BOUNDS.height(), - displayController, - ) + verify(desktopModeEventLogger, times(2)) + .logTaskResizingEnded( + ResizeTrigger.TILING_DIVIDER, + DesktopModeEventLogger.Companion.InputMethod.UNKNOWN_INPUT_METHOD, + task1, + BOUNDS.width(), + BOUNDS.height(), + displayController, + ) } @Test diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index 56191c01aaef..87a43fcb0855 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -29,6 +29,7 @@ using namespace std; extern int register_android_graphics_Bitmap(JNIEnv*); extern int register_android_graphics_BitmapFactory(JNIEnv*); extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*); +extern int register_android_graphics_RuntimeXfermode(JNIEnv*); extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Camera(JNIEnv* env); extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); @@ -131,6 +132,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = { {"android.graphics.RenderNode", REG_JNI(register_android_view_RenderNode)}, {"android.graphics.Shader", REG_JNI(register_android_graphics_Shader)}, {"android.graphics.RenderEffect", REG_JNI(register_android_graphics_RenderEffect)}, + {"android.graphics.RuntimeXfermode", REG_JNI(register_android_graphics_RuntimeXfermode)}, {"android.graphics.Typeface", REG_JNI(register_android_graphics_Typeface)}, {"android.graphics.YuvImage", REG_JNI(register_android_graphics_YuvImage)}, {"android.graphics.animation.NativeInterpolatorFactory", diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java index 4ae8daa63e1d..6a33b374b21c 100644 --- a/media/java/android/media/MediaRoute2ProviderService.java +++ b/media/java/android/media/MediaRoute2ProviderService.java @@ -759,12 +759,29 @@ public abstract class MediaRoute2ProviderService extends Service { /** * Updates routes of the provider and notifies the system media router service. + * + * @throws IllegalArgumentException If {@code routes} contains a route that {@link + * MediaRoute2Info#getSupportedRoutingTypes() supports} both system media routing and remote + * routing but doesn't contain any {@link MediaRoute2Info#getDeduplicationIds() + * deduplication ids}. */ public final void notifyRoutes(@NonNull Collection<MediaRoute2Info> routes) { requireNonNull(routes, "routes must not be null"); List<MediaRoute2Info> sanitizedRoutes = new ArrayList<>(routes.size()); for (MediaRoute2Info route : routes) { + if (Flags.enableMirroringInMediaRouter2() + && route.supportsRemoteRouting() + && route.supportsSystemMediaRouting() + && route.getDeduplicationIds().isEmpty()) { + String errorMessage = + TextUtils.formatSimple( + "Route with id='%s' name='%s' supports both system media and remote" + + " type routing, but doesn't contain a deduplication id, which" + + " it needs. You can add the route id as a deduplication id.", + route.getOriginalId(), route.getName()); + throw new IllegalArgumentException(errorMessage); + } if (route.isSystemRouteType()) { Log.w( TAG, diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 275972495206..f6dc41ed128a 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -89,7 +89,7 @@ ASurfaceControl* ASurfaceControl_createFromWindow(ANativeWindow* window, const c CHECK_NOT_NULL(window); CHECK_NOT_NULL(debug_name); - sp<SurfaceComposerClient> client = new SurfaceComposerClient(); + sp<SurfaceComposerClient> client = sp<SurfaceComposerClient>::make(); if (client->initCheck() != NO_ERROR) { return nullptr; } diff --git a/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt index bb3a04df6f36..705d9e1cf442 100644 --- a/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt +++ b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt @@ -127,7 +127,7 @@ class UniverseProgressNotifier(val context: Context, val universe: Universe) { val eta = if (speed > 0) "%1.0fs".format(distToTarget / speed) else "???" builder.setContentTitle("headed to: ${target.name}") builder.setContentText( - "autopilot is ${autopilot.strategy.toLowerCase()}" + + "autopilot is ${autopilot.strategy.lowercase()}" + "\ndist: ${distToTarget}u // eta: $eta" ) // fun fact: ProgressStyle was originally EnRouteStyle diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java index d883fb0594e6..5170581aa382 100644 --- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java +++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java @@ -80,7 +80,14 @@ public class MainSwitchPreference extends TwoStatePreference implements OnChecke mainSwitchBar.setIconSpaceReserved(isIconSpaceReserved()); // To support onPreferenceChange callback, it needs to call callChangeListener() when // MainSwitchBar is clicked. - mainSwitchBar.setOnClickListener(view -> callChangeListener(isChecked())); + mainSwitchBar.setOnClickListener(view -> { + boolean isChecked = isChecked(); + if (!callChangeListener(isChecked)) { + // Change checked state back if listener doesn't like it. + // Note that CompoundButton maintains internal state to avoid infinite recursion. + mainSwitchBar.setChecked(!isChecked); + } + }); // Remove all listeners to 1. avoid triggering listener when update UI 2. prevent potential // listener leaking when the view holder is reused by RecyclerView @@ -88,7 +95,11 @@ public class MainSwitchPreference extends TwoStatePreference implements OnChecke mainSwitchBar.setChecked(isChecked()); mainSwitchBar.addOnSwitchChangeListener(this); - mainSwitchBar.show(); + if (isVisible()) { + mainSwitchBar.show(); + } else { + mainSwitchBar.hide(); + } } @Override @@ -101,7 +112,10 @@ public class MainSwitchPreference extends TwoStatePreference implements OnChecke /** * Adds a listener for switch changes + * + * @deprecated Use {@link #setOnPreferenceChangeListener(OnPreferenceChangeListener)} */ + @Deprecated public void addOnSwitchChangeListener(OnCheckedChangeListener listener) { if (!mSwitchChangeListeners.contains(listener)) { mSwitchChangeListeners.add(listener); @@ -110,7 +124,10 @@ public class MainSwitchPreference extends TwoStatePreference implements OnChecke /** * Remove a listener for switch changes + * + * @deprecated Use {@link #setOnPreferenceChangeListener(OnPreferenceChangeListener)} */ + @Deprecated public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) { mSwitchChangeListeners.remove(listener); } diff --git a/packages/SettingsLib/SpaPrivileged/tests/Android.bp b/packages/SettingsLib/SpaPrivileged/tests/unit/Android.bp index 458fcc97aa9b..458fcc97aa9b 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/Android.bp +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/Android.bp diff --git a/packages/SettingsLib/SpaPrivileged/tests/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/tests/unit/AndroidManifest.xml index 8d384e8ca02e..8d384e8ca02e 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/AndroidManifest.xml +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/AndroidManifest.xml diff --git a/packages/SettingsLib/SpaPrivileged/tests/res/values/strings.xml b/packages/SettingsLib/SpaPrivileged/tests/unit/res/values/strings.xml index bdc0ba8224de..bdc0ba8224de 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/res/values/strings.xml +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/res/values/strings.xml diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt index 9cb33d2e9b2c..9cb33d2e9b2c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt index 772f925c0a77..772f925c0a77 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverFlowTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt index 7220848eebff..7220848eebff 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt index 4221f9fb5111..4221f9fb5111 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt index fd4b189c51ff..fd4b189c51ff 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt index 4d9d6da582b1..4d9d6da582b1 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt index 74a7c146b2ab..74a7c146b2ab 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt index 9f80b92548d2..9f80b92548d2 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppOpsPermissionControllerTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt index 97c74411d945..97c74411d945 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppOpsRepositoryTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppRepositoryTest.kt index 70e405557dc7..70e405557dc7 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppRepositoryTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt index e8ec974bb0b8..e8ec974bb0b8 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfosTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfosTest.kt index 7f9e98b95fb7..7f9e98b95fb7 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfosTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/ApplicationInfosTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt index e10619e01c4e..e10619e01c4e 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/PackageManagerExtTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt index 7c928389d08f..7c928389d08f 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/PackageManagersTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt index 7ef11eb865ba..7ef11eb865ba 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/app/PermissionsChangedFlowTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt index f3245c9085e7..f3245c9085e7 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedModeTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt index cd747cc142c1..cd747cc142c1 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt index c1d298d0b613..c1d298d0b613 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt index ecc92f8f8d5c..ecc92f8f8d5c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureStringTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureStringTest.kt index e3d182bb5ace..e3d182bb5ace 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureStringTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureStringTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt index 72a5bd76e737..72a5bd76e737 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt index 4d90076f060e..4d90076f060e 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt index 73dd295de77b..73dd295de77b 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt index c6409e7738d6..c6409e7738d6 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt index d3cfb2d71116..d3cfb2d71116 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt index 60eccd987724..60eccd987724 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt index 4f42c8254c39..4f42c8254c39 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPageTest.kt index 798e2d49ff57..798e2d49ff57 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPageTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt index 79085af63c6d..79085af63c6d 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListTest.kt index 1818f2d92f9c..1818f2d92f9c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt index e450364a9ab2..e450364a9ab2 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreferenceTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreferenceTest.kt index 55c16bd20336..55c16bd20336 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreferenceTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreferenceTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreferenceTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreferenceTest.kt index eadf0ca0686d..eadf0ca0686d 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreferenceTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreferenceTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt index 1fd7ecf3cf40..1fd7ecf3cf40 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedTwoTargetSwitchPreferenceTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/preference/RestrictedTwoTargetSwitchPreferenceTest.kt index bdff89f6d69b..bdff89f6d69b 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedTwoTargetSwitchPreferenceTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/preference/RestrictedTwoTargetSwitchPreferenceTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt index 4068bceb1475..4068bceb1475 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/RestrictedTestUtils.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/tests/testutils/RestrictedTestUtils.kt index d5e8d6a5fa13..d5e8d6a5fa13 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/RestrictedTestUtils.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/tests/testutils/RestrictedTestUtils.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt index a7a153ba479c..a7a153ba479c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestTogglePermissionAppListModel.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/tests/testutils/TestTogglePermissionAppListModel.kt index 000743e7ef8a..000743e7ef8a 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestTogglePermissionAppListModel.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/tests/testutils/TestTogglePermissionAppListModel.kt diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestTogglePermissionAppListProvider.kt b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/tests/testutils/TestTogglePermissionAppListProvider.kt index 354bbf536a85..354bbf536a85 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestTogglePermissionAppListProvider.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/unit/src/com/android/settingslib/spaprivileged/tests/testutils/TestTogglePermissionAppListProvider.kt diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index a029f56cf1d7..418a76ffb534 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -249,3 +249,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "audio_stream_media_service_by_receive_state" + namespace: "cross_device_experiences" + description: "Start or update audio stream media service by receive state" + bug: "398700619" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index e78a69239334..ae9ad958b287 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -51,6 +51,8 @@ import com.android.settingslib.widget.AdaptiveOutlineDrawable; import com.google.common.collect.ImmutableSet; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -1268,4 +1270,15 @@ public class BluetoothUtils { return false; } + + /** Gets key missing count of the device. This is a workaround before the API is rolled out. */ + public static Integer getKeyMissingCount(BluetoothDevice device) { + try { + Method m = BluetoothDevice.class.getDeclaredMethod("getKeyMissingCount"); + return (int) m.invoke(device); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + Log.w(TAG, "error happens when getKeyMissingCount."); + return null; + } + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchPreferenceTest.java index a47b4d5c354f..093833ec41cf 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchPreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchPreferenceTest.java @@ -18,13 +18,22 @@ package com.android.settingslib.widget; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import android.content.Context; import android.view.View; import android.widget.TextView; +import androidx.preference.PreferenceDataStore; import androidx.preference.PreferenceViewHolder; import androidx.test.core.app.ApplicationProvider; +import com.android.settingslib.preference.PreferenceScreenFactory; import com.android.settingslib.widget.mainswitch.R; import org.junit.Before; @@ -67,4 +76,31 @@ public class MainSwitchPreferenceTest { assertThat(mRootView.<MainSwitchBar>requireViewById( R.id.settingslib_main_switch_bar).isChecked()).isTrue(); } + + @Test + public void setOnPreferenceChangeListener() { + // Attach preference to preference screen, otherwise `Preference.performClick` does not + // interact with underlying datastore + new PreferenceScreenFactory(mContext).getOrCreatePreferenceScreen().addPreference( + mPreference); + + PreferenceDataStore preferenceDataStore = mock(PreferenceDataStore.class); + // always return the provided default value + when(preferenceDataStore.getBoolean(any(), anyBoolean())).thenAnswer( + invocation -> invocation.getArguments()[1]); + mPreference.setPreferenceDataStore(preferenceDataStore); + + String key = "key"; + mPreference.setKey(key); + mPreference.setOnPreferenceChangeListener((preference, newValue) -> false); + mPreference.onBindViewHolder(mHolder); + + mPreference.performClick(); + verify(preferenceDataStore, never()).putBoolean(any(), anyBoolean()); + + mPreference.setOnPreferenceChangeListener((preference, newValue) -> true); + + mPreference.performClick(); + verify(preferenceDataStore).putBoolean(any(), anyBoolean()); + } } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 3c70fc15485a..c0105298899b 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -271,8 +271,6 @@ public class SecureSettings { Settings.Secure.DEFAULT_NOTE_TASK_PROFILE, Settings.Secure.CREDENTIAL_SERVICE, Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, - Settings.Secure.EVEN_DIMMER_ACTIVATED, - Settings.Secure.EVEN_DIMMER_MIN_NITS, Settings.Secure.STYLUS_POINTER_ICON_ENABLED, Settings.Secure.CAMERA_EXTENSIONS_FALLBACK, Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index c09e45ed81a6..0ffdf53f2036 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -114,9 +114,6 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FONT_WEIGHT_ADJUSTMENT, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.REDUCE_BRIGHT_COLORS_LEVEL, PERCENTAGE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.REDUCE_BRIGHT_COLORS_PERSIST_ACROSS_REBOOTS, BOOLEAN_VALIDATOR); - VALIDATORS.put(Secure.EVEN_DIMMER_ACTIVATED, BOOLEAN_VALIDATOR); - VALIDATORS.put(Secure.EVEN_DIMMER_MIN_NITS, - new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE)); VALIDATORS.put(Secure.TTS_DEFAULT_RATE, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.TTS_DEFAULT_PITCH, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.TTS_DEFAULT_SYNTH, PACKAGE_NAME_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java index de7c450d8d39..7c975b750639 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java @@ -72,6 +72,7 @@ import java.util.regex.Pattern; public final class DeviceConfigService extends Binder { private static final List<String> sAconfigTextProtoFilesOnDevice = List.of( "/system/etc/aconfig_flags.pb", + "/system_ext/etc/aconfig_flags.pb", "/product/etc/aconfig_flags.pb", "/vendor/etc/aconfig_flags.pb"); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 167674a451b3..e07832eea65e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2189,15 +2189,6 @@ class SettingsProtoDumpUtil { Settings.Secure.ENHANCED_VOICE_PRIVACY_ENABLED, SecureSettingsProto.ENHANCED_VOICE_PRIVACY_ENABLED); - final long evenDimmerToken = p.start(SecureSettingsProto.EVEN_DIMMER); - dumpSetting(s, p, - Settings.Secure.EVEN_DIMMER_ACTIVATED, - SecureSettingsProto.EvenDimmer.EVEN_DIMMER_ACTIVATED); - dumpSetting(s, p, - Settings.Secure.EVEN_DIMMER_MIN_NITS, - SecureSettingsProto.EvenDimmer.EVEN_DIMMER_MIN_NITS); - p.end(evenDimmerToken); - dumpSetting(s, p, Settings.Secure.EM_VALUE, SecureSettingsProto.Accessibility.EM_VALUE); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index bc281eea39d8..4a225bdbd7e5 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -396,6 +396,8 @@ public class SettingsProvider extends ContentProvider { private volatile SystemConfigManager mSysConfigManager; + private PackageMonitor mPackageMonitor; + @GuardedBy("mLock") private boolean mSyncConfigDisabledUntilReboot; @@ -403,6 +405,7 @@ public class SettingsProvider extends ContentProvider { @EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.S) private static final long ENFORCE_READ_PERMISSION_FOR_MULTI_SIM_DATA_CALL = 172670679L; + @Override public boolean onCreate() { Settings.setInSystemServer(); @@ -1036,7 +1039,7 @@ public class SettingsProvider extends ContentProvider { } }, userFilter); - PackageMonitor monitor = new PackageMonitor() { + mPackageMonitor = new PackageMonitor() { @Override public void onPackageRemoved(String packageName, int uid) { synchronized (mLock) { @@ -1062,7 +1065,7 @@ public class SettingsProvider extends ContentProvider { }; // package changes - monitor.register(getContext(), BackgroundThread.getHandler().getLooper(), + mPackageMonitor.register(getContext(), BackgroundThread.getHandler().getLooper(), UserHandle.ALL, true); } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 99c4e21c6053..17c13b78778c 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -158,6 +158,7 @@ public class SettingsState { private static final List<String> sAconfigTextProtoFilesOnDevice = List.of( "/system/etc/aconfig_flags.pb", + "/system_ext/etc/aconfig_flags.pb", "/product/etc/aconfig_flags.pb", "/vendor/etc/aconfig_flags.pb"); diff --git a/packages/SystemUI/BUILD_OWNERS b/packages/SystemUI/BUILD_OWNERS new file mode 100644 index 000000000000..4aadee173388 --- /dev/null +++ b/packages/SystemUI/BUILD_OWNERS @@ -0,0 +1,22 @@ +# Build file owners for System UI. Owners should consider the following: +# +# - Does the change negatively affect developer builds? Will it make +# the build slower? +# +# - Does the change add unnecessary dependencies or compilation steps +# that will be difficult to refactor? +# +# For more information, see http://go/sysui-build-owners + +dsandler@android.com + +caitlinshk@google.com +ccross@android.com +cinek@google.com +jernej@google.com +mankoff@google.com +nicomazz@google.com +peskal@google.com +pixel@google.com +saff@google.com +vadimt@google.com diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index f5c0233d56b1..ab3fa1b06255 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -125,3 +125,6 @@ silvajordan@google.com uwaisashraf@google.com vinayjoglekar@google.com willosborn@google.com + +per-file *.mk,{**/,}Android.bp = set noparent +per-file *.mk,{**/,}Android.bp = file:BUILD_OWNERS diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt index f4e03613169a..96feeedb8793 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GSFAxes.kt @@ -91,8 +91,8 @@ object GSFAxes { private val AXIS_MAP = listOf(WEIGHT, WIDTH, SLANT, ROUND, GRADE, OPTICAL_SIZE, ITALIC) - .map { def -> def.tag.toLowerCase() to def } + .map { def -> def.tag.lowercase() to def } .toMap() - fun getAxis(axis: String): AxisDefinition? = AXIS_MAP[axis.toLowerCase()] + fun getAxis(axis: String): AxisDefinition? = AXIS_MAP[axis.lowercase()] } diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DoNotDirectlyConstructKosmosDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DoNotDirectlyConstructKosmosDetector.kt new file mode 100644 index 000000000000..40fac0d05b96 --- /dev/null +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DoNotDirectlyConstructKosmosDetector.kt @@ -0,0 +1,79 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.intellij.psi.PsiMethod +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.getContainingUClass + +/** + * Detects direct construction of `Kosmos()` in subclasses of SysuiTestCase, which can and should + * use `testKosmos`. See go/thetiger + */ +class DoNotDirectlyConstructKosmosDetector : Detector(), SourceCodeScanner { + override fun getApplicableConstructorTypes() = listOf("com.android.systemui.kosmos.Kosmos") + + override fun visitConstructor( + context: JavaContext, + node: UCallExpression, + constructor: PsiMethod, + ) { + val superClassNames = + node.getContainingUClass()?.superTypes.orEmpty().map { it.resolve()?.qualifiedName } + if (superClassNames.contains("com.android.systemui.SysuiTestCase")) { + context.report( + issue = ISSUE, + scope = node, + location = context.getLocation(node.methodIdentifier), + message = "Prefer testKosmos to direct Kosmos() in sysui tests. go/testkosmos", + ) + } + super.visitConstructor(context, node, constructor) + } + + companion object { + @JvmStatic + val ISSUE = + Issue.create( + id = "DoNotDirectlyConstructKosmos", + briefDescription = + "Prefer testKosmos to direct Kosmos() in sysui tests. go/testkosmos", + explanation = + """ + SysuiTestCase.testKosmos allows us to pre-populate a Kosmos instance with + team-standard fixture values, and makes it easier to make centralized changes + when necessary. See go/testkosmos + """, + category = Category.TESTING, + priority = 8, + severity = Severity.WARNING, + implementation = + Implementation( + DoNotDirectlyConstructKosmosDetector::class.java, + Scope.JAVA_FILE_SCOPE, + ), + ) + } +} diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunTestShouldUseKosmosDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunTestShouldUseKosmosDetector.kt index 4927fb9dc67d..13ffa6c5deaa 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunTestShouldUseKosmosDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RunTestShouldUseKosmosDetector.kt @@ -29,16 +29,12 @@ import com.intellij.psi.PsiMethod import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.getContainingUFile -/** - * Detects test function naming violations regarding use of the backtick-wrapped space-allowed - * feature of Kotlin functions. - */ +/** Detects use of `TestScope.runTest` when we should use `Kosmos.runTest` by go/kosmos-runtest */ class RunTestShouldUseKosmosDetector : Detector(), SourceCodeScanner { override fun getApplicableMethodNames() = listOf("runTest") override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { if (method.getReceiver()?.qualifiedName == "kotlinx.coroutines.test.TestScope") { - val imports = node.getContainingUFile()?.imports.orEmpty().mapNotNull { it.importReference?.asSourceString() diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DoNotDirectlyConstructKosmosTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DoNotDirectlyConstructKosmosTest.kt new file mode 100644 index 000000000000..20f6bcbdbbfe --- /dev/null +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DoNotDirectlyConstructKosmosTest.kt @@ -0,0 +1,104 @@ +/* + * 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.internal.systemui.lint + +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestFiles +import com.android.tools.lint.checks.infrastructure.TestLintResult +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue +import org.junit.Test + +class DoNotDirectlyConstructKosmosTest : SystemUILintDetectorTest() { + override fun getDetector(): Detector = DoNotDirectlyConstructKosmosDetector() + + override fun getIssues(): List<Issue> = listOf(DoNotDirectlyConstructKosmosDetector.ISSUE) + + @Test + fun wronglyTriesToDirectlyConstructKosmos() { + val runOnSource = + runOnSource( + """ + package test.pkg.name + + import com.android.systemui.kosmos.Kosmos + import com.android.systemui.SysuiTestCase + + class MyTest: SysuiTestCase { + val kosmos = Kosmos() + } + """ + ) + + runOnSource + .expectWarningCount(1) + .expect( + """ + src/test/pkg/name/MyTest.kt:7: Warning: Prefer testKosmos to direct Kosmos() in sysui tests. go/testkosmos [DoNotDirectlyConstructKosmos] + val kosmos = Kosmos() + ~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun okToConstructKosmosIfNotInSysuiTestCase() { + val runOnSource = + runOnSource( + """ + package test.pkg.name + + import com.android.systemui.kosmos.Kosmos + + class MyTest { + val kosmos = Kosmos() + } + """ + ) + + runOnSource.expectWarningCount(0) + } + + private fun runOnSource(source: String): TestLintResult { + return lint() + .files(TestFiles.kotlin(source).indented(), kosmosStub, sysuiTestCaseStub) + .issues(DoNotDirectlyConstructKosmosDetector.ISSUE) + .run() + } + + companion object { + private val kosmosStub: TestFile = + kotlin( + """ + package com.android.systemui.kosmos + + class Kosmos + """ + ) + + private val sysuiTestCaseStub: TestFile = + kotlin( + """ + package com.android.systemui + + class SysuiTestCase + """ + ) + } +} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt index 3f2f84b95977..827096996f0b 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt @@ -17,13 +17,20 @@ package com.android.compose.animation import androidx.compose.foundation.gestures.Orientation +import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.layout +import androidx.compose.ui.node.LayoutModifierNode +import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import kotlin.math.roundToInt /** A component that can bounce in one dimension, for instance when it is tapped. */ +@Stable interface Bounceable { val bounce: Dp } @@ -46,6 +53,7 @@ interface Bounceable { * RTL layouts) side. This can be used for grids for which the last item does not align perfectly * with the end of the grid. */ +@Stable fun Modifier.bounceable( bounceable: Bounceable, previousBounceable: Bounceable?, @@ -53,7 +61,47 @@ fun Modifier.bounceable( orientation: Orientation, bounceEnd: Boolean = nextBounceable != null, ): Modifier { - return layout { measurable, constraints -> + return this then + BounceableElement(bounceable, previousBounceable, nextBounceable, orientation, bounceEnd) +} + +private data class BounceableElement( + private val bounceable: Bounceable, + private val previousBounceable: Bounceable?, + private val nextBounceable: Bounceable?, + private val orientation: Orientation, + private val bounceEnd: Boolean, +) : ModifierNodeElement<BounceableNode>() { + override fun create(): BounceableNode { + return BounceableNode( + bounceable, + previousBounceable, + nextBounceable, + orientation, + bounceEnd, + ) + } + + override fun update(node: BounceableNode) { + node.bounceable = bounceable + node.previousBounceable = previousBounceable + node.nextBounceable = nextBounceable + node.orientation = orientation + node.bounceEnd = bounceEnd + } +} + +private class BounceableNode( + var bounceable: Bounceable, + var previousBounceable: Bounceable?, + var nextBounceable: Bounceable?, + var orientation: Orientation, + var bounceEnd: Boolean = nextBounceable != null, +) : Modifier.Node(), LayoutModifierNode { + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { // The constraints in the orientation should be fixed, otherwise there is no way to know // what the size of our child node will be without this animation code. checkFixedSize(constraints, orientation) @@ -61,10 +109,12 @@ fun Modifier.bounceable( var sizePrevious = 0f var sizeNext = 0f + val previousBounceable = previousBounceable if (previousBounceable != null) { sizePrevious += bounceable.bounce.toPx() - previousBounceable.bounce.toPx() } + val nextBounceable = nextBounceable if (nextBounceable != null) { sizeNext += bounceable.bounce.toPx() - nextBounceable.bounce.toPx() } else if (bounceEnd) { @@ -84,7 +134,7 @@ fun Modifier.bounceable( // constraints, otherwise the parent will automatically center this node given the // size that it expects us to be. This allows us to then place the element where we // want it to be. - layout(idleWidth, placeable.height) { + return layout(idleWidth, placeable.height) { placeable.placeRelative(-sizePrevious.roundToInt(), 0) } } @@ -95,7 +145,7 @@ fun Modifier.bounceable( constraints.copy(minHeight = animatedHeight, maxHeight = animatedHeight) val placeable = measurable.measure(animatedConstraints) - layout(placeable.width, idleHeight) { + return layout(placeable.width, idleHeight) { placeable.placeRelative(0, -sizePrevious.roundToInt()) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/PagerDots.kt index 91f1477d5325..172d88af4cc8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/PagerDots.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.panels.ui.compose +package com.android.systemui.common.ui.compose import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.Canvas @@ -43,9 +43,9 @@ import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import com.android.app.tracing.coroutines.launchTraced as launch import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch import platform.test.motion.compose.values.MotionTestValueKey import platform.test.motion.compose.values.motionTestValues diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt index 9eb78e14ab4e..b1afb161f33d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt @@ -16,7 +16,7 @@ package com.android.systemui.compose.modifiers -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics @@ -26,7 +26,11 @@ import androidx.compose.ui.semantics.testTagsAsResourceId * Set a test tag on this node so that it is associated with [resId]. This node will then be * accessible by integration tests using `sysuiResSelector(resId)`. */ -@OptIn(ExperimentalComposeUiApi::class) +@Stable fun Modifier.sysuiResTag(resId: String): Modifier { - return this.semantics { testTagsAsResourceId = true }.testTag("com.android.systemui:id/$resId") + // TODO(b/372412931): Only compose the semantics modifier once, at the root of the SystemUI + // window. + return this.then(TestTagAsResourceIdModifier).testTag("com.android.systemui:id/$resId") } + +private val TestTagAsResourceIdModifier = Modifier.semantics { testTagsAsResourceId = true } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt index 60c017227334..216f0a74e1c7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt @@ -141,7 +141,7 @@ fun FooterActions( mutableStateOf<FooterActionsForegroundServicesButtonViewModel?>(null) } var userSwitcher by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) } - var power by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) } + var power by remember { mutableStateOf(viewModel.initialPower()) } LaunchedEffect( context, @@ -218,23 +218,19 @@ fun FooterActions( } val useModifierBasedExpandable = remember { QSComposeFragment.isEnabled } - security?.let { SecurityButton(it, useModifierBasedExpandable, Modifier.weight(1f)) } - foregroundServices?.let { ForegroundServicesButton(it, useModifierBasedExpandable) } - userSwitcher?.let { - IconButton( - it, - useModifierBasedExpandable, - Modifier.sysuiResTag("multi_user_switch"), - ) - } + SecurityButton({ security }, useModifierBasedExpandable, Modifier.weight(1f)) + ForegroundServicesButton({ foregroundServices }, useModifierBasedExpandable) IconButton( - viewModel.settings, + { userSwitcher }, + useModifierBasedExpandable, + Modifier.sysuiResTag("multi_user_switch"), + ) + IconButton( + { viewModel.settings }, useModifierBasedExpandable, Modifier.sysuiResTag("settings_button_container"), ) - power?.let { - IconButton(it, useModifierBasedExpandable, Modifier.sysuiResTag("pm_lite")) - } + IconButton({ power }, useModifierBasedExpandable, Modifier.sysuiResTag("pm_lite")) } } } @@ -242,10 +238,11 @@ fun FooterActions( /** The security button. */ @Composable private fun SecurityButton( - model: FooterActionsSecurityButtonViewModel, + model: () -> FooterActionsSecurityButtonViewModel?, useModifierBasedExpandable: Boolean, modifier: Modifier = Modifier, ) { + val model = model() ?: return val onClick: ((Expandable) -> Unit)? = model.onClick?.let { onClick -> val context = LocalContext.current @@ -265,9 +262,10 @@ private fun SecurityButton( /** The foreground services button. */ @Composable private fun RowScope.ForegroundServicesButton( - model: FooterActionsForegroundServicesButtonViewModel, + model: () -> FooterActionsForegroundServicesButtonViewModel?, useModifierBasedExpandable: Boolean, ) { + val model = model() ?: return if (model.displayText) { TextButton( Icon.Resource(R.drawable.ic_info_outline, contentDescription = null), @@ -291,6 +289,17 @@ private fun RowScope.ForegroundServicesButton( /** A button with an icon. */ @Composable fun IconButton( + model: () -> FooterActionsButtonViewModel?, + useModifierBasedExpandable: Boolean, + modifier: Modifier = Modifier, +) { + val model = model() ?: return + IconButton(model, useModifierBasedExpandable, modifier) +} + +/** A button with an icon. */ +@Composable +fun IconButton( model: FooterActionsButtonViewModel, useModifierBasedExpandable: Boolean, modifier: Modifier = Modifier, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index 547461e5faf2..a0216268308c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -49,6 +49,8 @@ import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.content.state.TransitionState +import com.android.compose.modifiers.thenIf import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton @@ -258,7 +260,11 @@ fun ContentScope.QuickSettingsLayout( BrightnessSliderContainer( viewModel = viewModel.brightnessSliderViewModel, containerColor = OverlayShade.Colors.PanelBackground, - modifier = Modifier.systemGestureExclusionInShade().fillMaxWidth(), + modifier = + Modifier.systemGestureExclusionInShade( + enabled = { layoutState.transitionState is TransitionState.Idle } + ) + .fillMaxWidth(), ) Box { @@ -289,18 +295,20 @@ object QuickSettingsShade { * right. */ @Composable - fun Modifier.systemGestureExclusionInShade(): Modifier { + fun Modifier.systemGestureExclusionInShade(enabled: () -> Boolean): Modifier { val density = LocalDensity.current - return systemGestureExclusion { layoutCoordinates -> - val sidePadding = with(density) { Dimensions.Padding.toPx() } - Rect( - offset = Offset(x = -sidePadding, y = 0f), - size = - Size( - width = layoutCoordinates.size.width.toFloat() + 2 * sidePadding, - height = layoutCoordinates.size.height.toFloat(), - ), - ) + return thenIf(enabled()) { + Modifier.systemGestureExclusion { layoutCoordinates -> + val sidePadding = with(density) { Dimensions.Padding.toPx() } + Rect( + offset = Offset(x = -sidePadding, y = 0f), + size = + Size( + width = layoutCoordinates.size.width.toFloat() + 2 * sidePadding, + height = layoutCoordinates.size.height.toFloat(), + ), + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 0c502e6f492d..7015f79e4a9f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -56,6 +56,7 @@ import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.view.SceneJankMonitor import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.isFullWidthShade import javax.inject.Provider @@ -100,9 +101,13 @@ fun SceneContainer( rememberActivated(traceName = "sceneJankMonitor") { sceneJankMonitorFactory.create() } val hapticFeedback = LocalHapticFeedback.current + val shadeExpansionMotion = OverlayShade.rememberShadeExpansionMotion() val sceneTransitions = - remember(hapticFeedback) { - transitionsBuilder.build(viewModel.hapticsViewModel.getRevealHaptics(hapticFeedback)) + remember(hapticFeedback, shadeExpansionMotion) { + transitionsBuilder.build( + shadeExpansionMotion, + viewModel.hapticsViewModel.getRevealHaptics(hapticFeedback), + ) } val state = diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 7fe19fe70a25..9b45ef693ce6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -6,6 +6,7 @@ import com.android.compose.animation.scene.TransitionKey import com.android.compose.animation.scene.reveal.ContainerRevealHaptics import com.android.compose.animation.scene.transitions import com.android.internal.jank.Cuj +import com.android.mechanics.behavior.EdgeContainerExpansionSpec import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes @@ -48,7 +49,10 @@ import com.android.systemui.shade.ui.composable.Shade * Please keep the list sorted alphabetically. */ class SceneContainerTransitions : SceneContainerTransitionsBuilder { - override fun build(revealHaptics: ContainerRevealHaptics): SceneTransitions { + override fun build( + shadeExpansionMotion: EdgeContainerExpansionSpec, + revealHaptics: ContainerRevealHaptics, + ): SceneTransitions { return transitions { interruptionHandler = DefaultInterruptionHandler @@ -201,13 +205,19 @@ class SceneContainerTransitions : SceneContainerTransitionsBuilder { Overlays.NotificationsShade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, // NOTYPO ) { - toNotificationsShadeTransition(revealHaptics = revealHaptics) + toNotificationsShadeTransition( + shadeExpansionMotion = shadeExpansionMotion, + revealHaptics = revealHaptics, + ) } to( Overlays.QuickSettingsShade, cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, // NOTYPO ) { - toQuickSettingsShadeTransition(revealHaptics = revealHaptics) + toQuickSettingsShadeTransition( + shadeExpansionMotion = shadeExpansionMotion, + revealHaptics = revealHaptics, + ) } from( Scenes.Gone, @@ -215,7 +225,11 @@ class SceneContainerTransitions : SceneContainerTransitionsBuilder { key = SlightlyFasterShadeCollapse, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, // NOTYPO ) { - toNotificationsShadeTransition(durationScale = 0.9, revealHaptics = revealHaptics) + toNotificationsShadeTransition( + durationScale = 0.9, + shadeExpansionMotion = shadeExpansionMotion, + revealHaptics = revealHaptics, + ) } from( Scenes.Gone, @@ -223,7 +237,11 @@ class SceneContainerTransitions : SceneContainerTransitionsBuilder { key = SlightlyFasterShadeCollapse, cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, // NOTYPO ) { - toQuickSettingsShadeTransition(durationScale = 0.9, revealHaptics = revealHaptics) + toQuickSettingsShadeTransition( + durationScale = 0.9, + shadeExpansionMotion = shadeExpansionMotion, + revealHaptics = revealHaptics, + ) } from( Scenes.Lockscreen, @@ -231,7 +249,11 @@ class SceneContainerTransitions : SceneContainerTransitionsBuilder { key = SlightlyFasterShadeCollapse, cuj = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, // NOTYPO ) { - toNotificationsShadeTransition(durationScale = 0.9, revealHaptics = revealHaptics) + toNotificationsShadeTransition( + durationScale = 0.9, + shadeExpansionMotion = shadeExpansionMotion, + revealHaptics = revealHaptics, + ) } from( Scenes.Lockscreen, @@ -239,7 +261,11 @@ class SceneContainerTransitions : SceneContainerTransitionsBuilder { key = SlightlyFasterShadeCollapse, cuj = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, // NOTYPO ) { - toQuickSettingsShadeTransition(durationScale = 0.9, revealHaptics = revealHaptics) + toQuickSettingsShadeTransition( + durationScale = 0.9, + shadeExpansionMotion = shadeExpansionMotion, + revealHaptics = revealHaptics, + ) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitionsBuilder.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitionsBuilder.kt index 13d3456baa74..eb5548d45247 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitionsBuilder.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitionsBuilder.kt @@ -19,6 +19,7 @@ package com.android.systemui.scene.ui.composable import com.android.compose.animation.scene.SceneTransitions import com.android.compose.animation.scene.reveal.ContainerRevealHaptics import com.android.compose.animation.scene.transitions +import com.android.mechanics.behavior.EdgeContainerExpansionSpec /** * Builder of the comprehensive definition of all transitions between scenes and overlays in the @@ -27,7 +28,10 @@ import com.android.compose.animation.scene.transitions interface SceneContainerTransitionsBuilder { /** Build the [SceneContainer] transitions spec. */ - fun build(revealHaptics: ContainerRevealHaptics): SceneTransitions + fun build( + shadeExpansionMotion: EdgeContainerExpansionSpec, + revealHaptics: ContainerRevealHaptics, + ): SceneTransitions } /** @@ -37,5 +41,8 @@ interface SceneContainerTransitionsBuilder { class ConstantSceneContainerTransitionsBuilder( private val transitions: SceneTransitions = transitions { /* No transitions */ } ) : SceneContainerTransitionsBuilder { - override fun build(revealHaptics: ContainerRevealHaptics): SceneTransitions = transitions + override fun build( + shadeExpansionMotion: EdgeContainerExpansionSpec, + revealHaptics: ContainerRevealHaptics, + ): SceneTransitions = transitions } 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 722af6ae4f34..9b4b91eb23c1 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 @@ -20,6 +20,7 @@ import androidx.compose.animation.core.tween import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.reveal.ContainerRevealHaptics import com.android.compose.animation.scene.reveal.verticalContainerReveal +import com.android.mechanics.behavior.EdgeContainerExpansionSpec import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys import com.android.systemui.notifications.ui.composable.NotificationsShade import com.android.systemui.scene.shared.model.Overlays @@ -28,6 +29,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toNotificationsShadeTransition( durationScale: Double = 1.0, + shadeExpansionMotion: EdgeContainerExpansionSpec, revealHaptics: ContainerRevealHaptics, ) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) @@ -38,7 +40,7 @@ fun TransitionBuilder.toNotificationsShadeTransition( elevateInContent = Overlays.NotificationsShade, ) - verticalContainerReveal(NotificationsShade.Elements.Panel, revealHaptics) + verticalContainerReveal(NotificationsShade.Elements.Panel, shadeExpansionMotion, revealHaptics) fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) } fractionRange(start = .5f) { fade(NotificationsShade.Elements.StatusBar) } 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 3cce99740e47..47dd85f17cee 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 @@ -20,17 +20,19 @@ import androidx.compose.animation.core.tween import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.reveal.ContainerRevealHaptics import com.android.compose.animation.scene.reveal.verticalContainerReveal +import com.android.mechanics.behavior.EdgeContainerExpansionSpec import com.android.systemui.qs.ui.composable.QuickSettingsShade import com.android.systemui.shade.ui.composable.OverlayShade import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toQuickSettingsShadeTransition( durationScale: Double = 1.0, + shadeExpansionMotion: EdgeContainerExpansionSpec, revealHaptics: ContainerRevealHaptics, ) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - verticalContainerReveal(QuickSettingsShade.Elements.Panel, revealHaptics) + verticalContainerReveal(QuickSettingsShade.Elements.Panel, shadeExpansionMotion, revealHaptics) fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) } fractionRange(start = .5f) { fade(QuickSettingsShade.Elements.StatusBar) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 7e7b6297406e..3446081fb123 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -37,24 +37,26 @@ import androidx.compose.foundation.layout.systemBarsIgnoringVisibility import androidx.compose.foundation.layout.waterfall import androidx.compose.foundation.layout.width import androidx.compose.foundation.overscroll -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.windowsizeclass.LocalWindowSizeClass +import com.android.mechanics.behavior.EdgeContainerExpansionSpec +import com.android.mechanics.behavior.edgeContainerExpansionBackground import com.android.systemui.res.R -import androidx.compose.ui.unit.Dp +import com.android.systemui.shade.ui.composable.OverlayShade.rememberShadeExpansionMotion /** Renders a lightweight shade UI container, as an overlay. */ @Composable @@ -110,23 +112,15 @@ private fun ContentScope.Panel( ) { Box( modifier = - modifier.clip(OverlayShade.Shapes.RoundedCornerPanel).disableSwipesWhenScrolling() + modifier + .disableSwipesWhenScrolling() + .edgeContainerExpansionBackground( + OverlayShade.Colors.PanelBackground, + rememberShadeExpansionMotion(), + ) ) { - Spacer( - modifier = - Modifier.element(OverlayShade.Elements.PanelBackground) - .matchParentSize() - .background( - color = OverlayShade.Colors.PanelBackground, - shape = OverlayShade.Shapes.RoundedCornerPanel, - ) - ) - Column { header?.invoke() - - // This content is intentionally rendered as a separate element from the background in - // order to allow for more flexibility when defining transitions. content() } } @@ -192,8 +186,6 @@ object OverlayShade { contentPicker = LowestZIndexContentPicker, placeAllCopies = true, ) - val PanelBackground = - ElementKey("OverlayShadePanelBackground", contentPicker = LowestZIndexContentPicker) } object Colors { @@ -205,13 +197,13 @@ object OverlayShade { object Dimensions { val PanelCornerRadius: Dp @Composable - @ReadOnlyComposable get() = - dimensionResource(R.dimen.overlay_shade_panel_shape_radius) + @ReadOnlyComposable + get() = dimensionResource(R.dimen.overlay_shade_panel_shape_radius) } - object Shapes { - val RoundedCornerPanel: RoundedCornerShape - @Composable - @ReadOnlyComposable get() = RoundedCornerShape(Dimensions.PanelCornerRadius) + @Composable + fun rememberShadeExpansionMotion(): EdgeContainerExpansionSpec { + val radius = Dimensions.PanelCornerRadius + return remember(radius) { EdgeContainerExpansionSpec(radius = radius) } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt index 2134510557d0..72f9bd5da742 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt @@ -16,28 +16,20 @@ package com.android.compose.animation.scene.reveal -import androidx.compose.animation.core.AnimationVector1D -import androidx.compose.animation.core.DeferredTargetAnimation -import androidx.compose.animation.core.ExperimentalAnimatableApi -import androidx.compose.animation.core.FiniteAnimationSpec -import androidx.compose.animation.core.VectorConverter -import androidx.compose.animation.core.spring -import androidx.compose.ui.unit.Dp +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastCoerceAtLeast -import androidx.compose.ui.util.fastCoerceAtMost import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.OverlayKey -import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance import com.android.compose.animation.scene.content.state.TransitionState +import com.android.compose.animation.scene.mechanics.MotionValueInput +import com.android.compose.animation.scene.mechanics.TransitionScopedMechanicsAdapter import com.android.compose.animation.scene.transformation.CustomPropertyTransformation import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.PropertyTransformationScope -import kotlin.math.roundToInt +import com.android.mechanics.MotionValue +import com.android.mechanics.behavior.EdgeContainerExpansionSpec import kotlinx.coroutines.CoroutineScope interface ContainerRevealHaptics { @@ -53,9 +45,15 @@ interface ContainerRevealHaptics { fun onRevealThresholdCrossed(revealed: Boolean) } -/** Animate the reveal of [container] by animating its size. */ +/** + * Animate the reveal of [container] by animating its size. + * + * This implicitly sets the [distance] of the transition to the target size of [container] + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) fun TransitionBuilder.verticalContainerReveal( container: ElementKey, + motionSpec: EdgeContainerExpansionSpec, haptics: ContainerRevealHaptics, ) { // Make the swipe distance be exactly the target height of the container. @@ -76,188 +74,73 @@ fun TransitionBuilder.verticalContainerReveal( (targetSizeInToContent?.height ?: targetSizeInFromContent?.height)?.toFloat() ?: 0f } - // TODO(b/376438969): Improve the motion of this gesture using Motion Mechanics. - - // The min distance to swipe before triggering the reveal spring. - val distanceThreshold = 80.dp - - // The minimum height of the container. - val minHeight = 10.dp - - // The amount removed from the container width at 0% progress. - val widthDelta = 140.dp - - // The ratio at which the distance is tracked before reaching the threshold, e.g. if the user - // drags 60dp then the height will be 60dp * 0.25f = 15dp. - val trackingRatio = 0.25f - - // The max progress starting from which the container should always be visible, even if we are - // animating the container out. This is used so that we don't immediately fade out the container - // when triggering a one-off animation that hides it. - val alphaProgressThreshold = 0.05f - - // The spring animating the size of the container. - val sizeSpec = spring<Float>(stiffness = 380f, dampingRatio = 0.9f) - - // The spring animating the alpha of the container. - val alphaSpec = spring<Float>(stiffness = 1200f, dampingRatio = 0.99f) - - // Size transformation. - transformation(container) { - VerticalContainerRevealSizeTransformation( - haptics, - distanceThreshold, - trackingRatio, - minHeight, - widthDelta, - sizeSpec, - ) - } - - // Alpha transformation. - transformation(container) { - ContainerRevealAlphaTransformation(alphaSpec, alphaProgressThreshold) - } -} - -@OptIn(ExperimentalAnimatableApi::class) -private class VerticalContainerRevealSizeTransformation( - private val haptics: ContainerRevealHaptics, - private val distanceThreshold: Dp, - private val trackingRatio: Float, - private val minHeight: Dp, - private val widthDelta: Dp, - private val spec: FiniteAnimationSpec<Float>, -) : CustomPropertyTransformation<IntSize> { - override val property = PropertyTransformation.Property.Size - - private val widthAnimation = DeferredTargetAnimation(Float.VectorConverter) - private val heightAnimation = DeferredTargetAnimation(Float.VectorConverter) - - private var previousHasReachedThreshold: Boolean? = null - - override fun PropertyTransformationScope.transform( - content: ContentKey, - element: ElementKey, - transition: TransitionState.Transition, - transitionScope: CoroutineScope, - ): IntSize { - // The distance to go to 100%. Note that we don't use - // TransitionState.HasOverscrollProperties.absoluteDistance because the transition will not - // implement HasOverscrollProperties if the transition is triggered and not gesture based. + // TODO(b/392534646) Add haptics back + val heightInput: MotionValueInput = { progress, content, element -> val idleSize = checkNotNull(element.targetSize(content)) - val userActionDistance = idleSize.height - val progress = transition.progressTo(content) - val distance = (progress * userActionDistance).fastCoerceAtLeast(0f) - val threshold = distanceThreshold.toPx() - - // Width. - val widthDelta = widthDelta.toPx() - val width = - (idleSize.width - widthDelta + - animateSize( - size = widthDelta, - distance = distance, - threshold = threshold, - transitionScope = transitionScope, - animation = widthAnimation, - )) - .roundToInt() - - // Height. - val minHeight = minHeight.toPx() - val height = - ( - // 1) The minimum size of the container. - minHeight + - - // 2) The animated size between the minimum size and the threshold. - animateSize( - size = threshold - minHeight, - distance = distance, - threshold = threshold, - transitionScope = transitionScope, - animation = heightAnimation, - ) + - - // 3) The remaining height after the threshold, tracking the finger. - (distance - threshold).fastCoerceAtLeast(0f)) - .roundToInt() - .fastCoerceAtMost(idleSize.height) - - // Haptics. - val hasReachedThreshold = distance >= threshold - if ( - previousHasReachedThreshold != null && - hasReachedThreshold != previousHasReachedThreshold && - transition.isUserInputOngoing - ) { - haptics.onRevealThresholdCrossed(revealed = hasReachedThreshold) - } - previousHasReachedThreshold = hasReachedThreshold - - return IntSize(width = width, height = height) + val targetHeight = idleSize.height.toFloat() + targetHeight * progress } - /** - * Animate a size up to [size], so that it is equal to 0f when distance is 0f and equal to - * [size] when `distance >= threshold`, taking the [trackingRatio] into account. - */ - @OptIn(ExperimentalAnimatableApi::class) - private fun animateSize( - size: Float, - distance: Float, - threshold: Float, - transitionScope: CoroutineScope, - animation: DeferredTargetAnimation<Float, AnimationVector1D>, - ): Float { - val trackingSize = distance.fastCoerceAtMost(threshold) / threshold * size * trackingRatio - val springTarget = - if (distance >= threshold) { - size * (1f - trackingRatio) - } else { - 0f + transformation(container) { + object : CustomPropertyTransformation<IntSize> { + override val property = PropertyTransformation.Property.Size + + val heightValue = + TransitionScopedMechanicsAdapter( + computeInput = heightInput, + stableThreshold = MotionValue.StableThresholdSpatial, + label = "verticalContainerReveal::height", + ) { _, _ -> + motionSpec.createHeightSpec(motionScheme, density = this) + } + val widthValue = + TransitionScopedMechanicsAdapter( + computeInput = heightInput, + stableThreshold = MotionValue.StableThresholdSpatial, + label = "verticalContainerReveal::width", + ) { content, element -> + val idleSize = checkNotNull(element.targetSize(content)) + val intrinsicWidth = idleSize.width.toFloat() + motionSpec.createWidthSpec(intrinsicWidth, motionScheme, density = this) + } + + override fun PropertyTransformationScope.transform( + content: ContentKey, + element: ElementKey, + transition: TransitionState.Transition, + transitionScope: CoroutineScope, + ): IntSize { + + val height = + with(heightValue) { update(content, element, transition, transitionScope) } + val width = + with(widthValue) { update(content, element, transition, transitionScope) } + + return IntSize(width.toInt(), height.toInt()) } - val springSize = animation.updateTarget(springTarget, transitionScope, spec) - return trackingSize + springSize - } -} - -@OptIn(ExperimentalAnimatableApi::class) -private class ContainerRevealAlphaTransformation( - private val spec: FiniteAnimationSpec<Float>, - private val progressThreshold: Float, -) : CustomPropertyTransformation<Float> { - override val property = PropertyTransformation.Property.Alpha - private val alphaAnimation = DeferredTargetAnimation(Float.VectorConverter) - - override fun PropertyTransformationScope.transform( - content: ContentKey, - element: ElementKey, - transition: TransitionState.Transition, - transitionScope: CoroutineScope, - ): Float { - return alphaAnimation.updateTarget(targetAlpha(transition, content), transitionScope, spec) - } - - private fun targetAlpha(transition: TransitionState.Transition, content: ContentKey): Float { - if (transition.isUserInputOngoing) { - return if (transition.progressTo(content) > 0f) 1f else 0f } + } - // The transition was committed (the user released their finger), so the alpha depends on - // whether we are animating towards the content (showing the container) or away from it - // (hiding the container). - val isShowingContainer = - when (content) { - is SceneKey -> transition.currentScene == content - is OverlayKey -> transition.currentOverlays.contains(content) + transformation(container) { + object : CustomPropertyTransformation<Float> { + + override val property = PropertyTransformation.Property.Alpha + val alphaValue = + TransitionScopedMechanicsAdapter( + computeInput = heightInput, + label = "verticalContainerReveal::alpha", + ) { _, _ -> + motionSpec.createAlphaSpec(motionScheme, density = this) + } + + override fun PropertyTransformationScope.transform( + content: ContentKey, + element: ElementKey, + transition: TransitionState.Transition, + transitionScope: CoroutineScope, + ): Float { + return with(alphaValue) { update(content, element, transition, transitionScope) } } - - return if (isShowingContainer || transition.progressTo(content) >= progressThreshold) { - 1f - } else { - 0f } } } diff --git a/packages/SystemUI/compose/scene/tests/goldens/motionValue_interruptedAnimation_completes.json b/packages/SystemUI/compose/scene/tests/goldens/motionValue_interruptedAnimation_completes.json index ce62ac3f4ee2..a2f8863a34f7 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/motionValue_interruptedAnimation_completes.json +++ b/packages/SystemUI/compose/scene/tests/goldens/motionValue_interruptedAnimation_completes.json @@ -24,7 +24,6 @@ 336, 352, 368, - 384, "after" ], "features": [ @@ -38,29 +37,28 @@ { "type": "not_found" }, - 175, - 175, - 174.00105, - 149.84001, - 114.73702, + 125, + 125, + 124.28647, + 107.02858, + 81.95502, 0, 0, 0, 0, - 10.212692, - 42.525528, - 77.174965, - 106.322296, - 128.37651, - 144.09671, - 154.88022, - 162.08202, - 166.79778, - 169.83923, - 171.77742, - 173.00056, - 173.76627, - 174.24236, + 7.2947845, + 30.375374, + 55.12497, + 75.944496, + 91.69751, + 102.92622, + 110.62873, + 115.772865, + 119.141266, + 121.313736, + 122.69816, + 123.57184, + 124.11877, { "type": "not_found" } diff --git a/packages/SystemUI/compose/scene/tests/goldens/motionValue_withAnimation_prolongsTransition.json b/packages/SystemUI/compose/scene/tests/goldens/motionValue_withAnimation_prolongsTransition.json index ac09ff3f359c..bda53bbf3e6c 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/motionValue_withAnimation_prolongsTransition.json +++ b/packages/SystemUI/compose/scene/tests/goldens/motionValue_withAnimation_prolongsTransition.json @@ -24,23 +24,23 @@ "name": "Foo_yOffset", "type": "float", "data_points": [ - 175, - 175, - 175, - 175, - 156.26086, - 121.784874, - 88.35684, - 61.32686, - 41.302353, - 27.215454, - 17.638702, - 11.284393, - 7.144104, - 4.4841614, - 2.7943878, - 1.7307587, - 1.0663452, + 125, + 125, + 125, + 125, + 111.61491, + 86.9892, + 63.112034, + 43.8049, + 29.501678, + 19.439606, + 12.599068, + 8.06028, + 5.102936, + 3.2029724, + 1.9959946, + 1.2362518, + 0.761673, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/motionValue_withoutAnimation_terminatesImmediately.json b/packages/SystemUI/compose/scene/tests/goldens/motionValue_withoutAnimation_terminatesImmediately.json index 5cf66a4aa88c..7def82f08d0c 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/motionValue_withoutAnimation_terminatesImmediately.json +++ b/packages/SystemUI/compose/scene/tests/goldens/motionValue_withoutAnimation_terminatesImmediately.json @@ -13,12 +13,12 @@ "name": "Foo_yOffset", "type": "float", "data_points": [ - 175, - 145.83333, - 116.666664, - 87.5, - 58.33333, - 29.166672, + 125, + 104.166664, + 83.33333, + 62.5, + 41.666664, + 20.833336, 0 ] } diff --git a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeEnter.json b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeEnter.json index 2df440912bfc..054b4a100dea 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeEnter.json +++ b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeEnter.json @@ -20,7 +20,7 @@ "height": 100 }, { - "width": 125.14286, + "width": 125.2, "height": 90 }, { @@ -28,7 +28,7 @@ "height": 80 }, { - "width": 175.14285, + "width": 175.2, "height": 70 }, { diff --git a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeExit.json b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeExit.json index 2b0a9541a394..ad46a8d14ede 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeExit.json +++ b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredSizeExit.json @@ -21,7 +21,7 @@ "height": 100 }, { - "width": 125.14286, + "width": 125.2, "height": 90 }, { @@ -29,7 +29,7 @@ "height": 80 }, { - "width": 175.14285, + "width": 175.2, "height": 70 }, { diff --git a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredWidthOnly.json b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredWidthOnly.json index 027df299d15e..9a97053eb821 100644 --- a/packages/SystemUI/compose/scene/tests/goldens/testAnchoredWidthOnly.json +++ b/packages/SystemUI/compose/scene/tests/goldens/testAnchoredWidthOnly.json @@ -20,7 +20,7 @@ "height": 60 }, { - "width": 125.14286, + "width": 125.2, "height": 60 }, { @@ -28,7 +28,7 @@ "height": 60 }, { - "width": 175.14285, + "width": 175.2, "height": 60 }, { diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json new file mode 100644 index 000000000000..0fcdfa3e1b53 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json @@ -0,0 +1,634 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432, + 448, + 464, + 480, + 496, + 512, + 528, + 544, + 560, + 576, + 592, + 608, + 624, + 640, + 656, + 672, + 688, + 704, + 720, + 736, + 752, + 768, + 784, + 800, + 816, + 832, + 848, + 864, + 880, + 896, + 912, + 928, + 944, + 960 + ], + "features": [ + { + "name": "RevealElement_position", + "type": "dpOffset", + "data_points": [ + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50.4 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 52.4, + "y": 50 + }, + { + "x": 56, + "y": 50 + }, + { + "x": 58.8, + "y": 50 + }, + { + "x": 60.8, + "y": 50 + }, + { + "x": 62, + "y": 50 + }, + { + "x": 62.8, + "y": 50 + }, + { + "x": 63.6, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + } + ] + }, + { + "name": "RevealElement_size", + "type": "dpSize", + "data_points": [ + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 393.2 + }, + { + "width": 188, + "height": 393.2 + }, + { + "width": 188, + "height": 386 + }, + { + "width": 188, + "height": 379.6 + }, + { + "width": 188, + "height": 372.8 + }, + { + "width": 188, + "height": 366.8 + }, + { + "width": 188, + "height": 360.4 + }, + { + "width": 188, + "height": 354 + }, + { + "width": 188, + "height": 347.6 + }, + { + "width": 188, + "height": 341.2 + }, + { + "width": 188, + "height": 341.2 + }, + { + "width": 188, + "height": 334 + }, + { + "width": 188, + "height": 328 + }, + { + "width": 188, + "height": 321.6 + }, + { + "width": 188, + "height": 315.2 + }, + { + "width": 188, + "height": 308.8 + }, + { + "width": 188, + "height": 302.4 + }, + { + "width": 188, + "height": 296 + }, + { + "width": 188, + "height": 289.6 + }, + { + "width": 188, + "height": 289.6 + }, + { + "width": 188, + "height": 282.8 + }, + { + "width": 188, + "height": 276.4 + }, + { + "width": 188, + "height": 270 + }, + { + "width": 188, + "height": 263.6 + }, + { + "width": 188, + "height": 257.2 + }, + { + "width": 188, + "height": 250.8 + }, + { + "width": 188, + "height": 244.4 + }, + { + "width": 188, + "height": 238 + }, + { + "width": 188, + "height": 238 + }, + { + "width": 188, + "height": 231.2 + }, + { + "width": 188, + "height": 224.8 + }, + { + "width": 188, + "height": 218.4 + }, + { + "width": 188, + "height": 212 + }, + { + "width": 188, + "height": 212 + }, + { + "width": 188, + "height": 192.4 + }, + { + "width": 188, + "height": 159.6 + }, + { + "width": 188, + "height": 124.4 + }, + { + "width": 188, + "height": 92.8 + }, + { + "width": 183.2, + "height": 66.4 + }, + { + "width": 176, + "height": 46 + }, + { + "width": 170.4, + "height": 39.2 + }, + { + "width": 166.8, + "height": 36 + }, + { + "width": 164, + "height": 31.6 + }, + { + "width": 162.4, + "height": 26.8 + }, + { + "width": 161.2, + "height": 22 + }, + { + "width": 160.4, + "height": 17.6 + }, + { + "width": 160, + "height": 14 + }, + { + "width": 160, + "height": 10.8 + }, + { + "width": 160, + "height": 8 + }, + { + "width": 160, + "height": 6 + }, + { + "width": 160, + "height": 4.4 + }, + { + "width": 160, + "height": 2.8 + }, + { + "width": 160, + "height": 2 + }, + { + "width": 160, + "height": 1.2 + }, + { + "width": 160, + "height": 0.8 + }, + { + "width": 160, + "height": 0.4 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 + } + ] + }, + { + "name": "RevealElement_alpha", + "type": "float", + "data_points": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0.9808927, + 0.8211168, + 0.61845565, + 0.43834114, + 0.29850912, + 0.19755232, + 0.12793064, + 0.08142871, + 0.051099956, + 0.031684637, + 0.019442618, + 0.011821032, + 0, + 0, + 0, + 0, + 0 + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json new file mode 100644 index 000000000000..3196334c5314 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json @@ -0,0 +1,624 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432, + 448, + 464, + 480, + 496, + 512, + 528, + 544, + 560, + 576, + 592, + 608, + 624, + 640, + 656, + 672, + 688, + 704, + 720, + 736, + 752, + 768, + 784, + 800, + 816, + 832, + 848, + 864, + 880, + 896, + 912, + 928, + 944 + ], + "features": [ + { + "name": "RevealElement_position", + "type": "dpOffset", + "data_points": [ + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50.8, + "y": 52 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 52.4, + "y": 50 + }, + { + "x": 55.6, + "y": 50 + }, + { + "x": 58.4, + "y": 50 + }, + { + "x": 60.4, + "y": 50 + }, + { + "x": 61.6, + "y": 50 + }, + { + "x": 62.8, + "y": 50 + }, + { + "x": 63.2, + "y": 50 + }, + { + "x": 63.6, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + } + ] + }, + { + "name": "RevealElement_size", + "type": "dpSize", + "data_points": [ + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 390 + }, + { + "width": 188, + "height": 390 + }, + { + "width": 188, + "height": 379.2 + }, + { + "width": 188, + "height": 371.2 + }, + { + "width": 188, + "height": 363.2 + }, + { + "width": 188, + "height": 355.2 + }, + { + "width": 188, + "height": 347.2 + }, + { + "width": 188, + "height": 339.2 + }, + { + "width": 188, + "height": 331.2 + }, + { + "width": 188, + "height": 323.2 + }, + { + "width": 188, + "height": 323.2 + }, + { + "width": 188, + "height": 314.8 + }, + { + "width": 188, + "height": 306.8 + }, + { + "width": 188, + "height": 298.8 + }, + { + "width": 188, + "height": 290.8 + }, + { + "width": 188, + "height": 282.8 + }, + { + "width": 188, + "height": 274.8 + }, + { + "width": 188, + "height": 266.8 + }, + { + "width": 188, + "height": 258.8 + }, + { + "width": 188, + "height": 258.8 + }, + { + "width": 188, + "height": 250.4 + }, + { + "width": 188, + "height": 242.4 + }, + { + "width": 188, + "height": 234.4 + }, + { + "width": 188, + "height": 226.4 + }, + { + "width": 188, + "height": 218.4 + }, + { + "width": 188, + "height": 210.4 + }, + { + "width": 188, + "height": 202.4 + }, + { + "width": 188, + "height": 194.4 + }, + { + "width": 188, + "height": 194.4 + }, + { + "width": 188, + "height": 185.6 + }, + { + "width": 188, + "height": 178 + }, + { + "width": 188, + "height": 170 + }, + { + "width": 188, + "height": 161.6 + }, + { + "width": 188, + "height": 161.6 + }, + { + "width": 188, + "height": 144.8 + }, + { + "width": 188, + "height": 118.8 + }, + { + "width": 188, + "height": 92 + }, + { + "width": 183.6, + "height": 68 + }, + { + "width": 176.8, + "height": 48.4 + }, + { + "width": 171.6, + "height": 39.6 + }, + { + "width": 167.6, + "height": 36.8 + }, + { + "width": 164.8, + "height": 32.4 + }, + { + "width": 162.8, + "height": 27.6 + }, + { + "width": 161.6, + "height": 22.8 + }, + { + "width": 160.8, + "height": 18.4 + }, + { + "width": 160.4, + "height": 14.4 + }, + { + "width": 160, + "height": 11.2 + }, + { + "width": 160, + "height": 8.4 + }, + { + "width": 160, + "height": 6.4 + }, + { + "width": 160, + "height": 4.4 + }, + { + "width": 160, + "height": 3.2 + }, + { + "width": 160, + "height": 2 + }, + { + "width": 160, + "height": 1.6 + }, + { + "width": 160, + "height": 0.8 + }, + { + "width": 160, + "height": 0.4 + }, + { + "width": 160, + "height": 0.4 + }, + { + "width": 160, + "height": 0 + } + ] + }, + { + "name": "RevealElement_alpha", + "type": "float", + "data_points": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0.9967737, + 0.86538374, + 0.66414475, + 0.47619528, + 0.32686388, + 0.21757984, + 0.14153665, + 0.09041709, + 0.05691254, + 0.035380244, + 0.02175957, + 0.01325649, + 0.008007765, + 0, + 0, + 0, + 0 + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json new file mode 100644 index 000000000000..4b0306853903 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json @@ -0,0 +1,544 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432, + 448, + 464, + 480, + 496, + 512, + 528, + 544, + 560, + 576, + 592, + 608, + 624, + 640, + 656, + 672, + 688, + 704, + 720, + 736, + 752, + 768, + 784, + 800, + 816 + ], + "features": [ + { + "name": "RevealElement_position", + "type": "dpOffset", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "x": 62.8, + "y": 50 + }, + { + "x": 62.8, + "y": 50 + }, + { + "x": 61.6, + "y": 50 + }, + { + "x": 60.8, + "y": 50 + }, + { + "x": 59.6, + "y": 50 + }, + { + "x": 58.4, + "y": 50 + }, + { + "x": 57.2, + "y": 50 + }, + { + "x": 56, + "y": 50 + }, + { + "x": 55.2, + "y": 50 + }, + { + "x": 54, + "y": 50 + }, + { + "x": 54, + "y": 50 + }, + { + "x": 52.8, + "y": 50 + }, + { + "x": 51.6, + "y": 50 + }, + { + "x": 50.4, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + } + ] + }, + { + "name": "RevealElement_size", + "type": "dpSize", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "width": 162.4, + "height": 1.6 + }, + { + "width": 162.4, + "height": 1.6 + }, + { + "width": 164.8, + "height": 3.2 + }, + { + "width": 166.8, + "height": 4.8 + }, + { + "width": 169.2, + "height": 6.4 + }, + { + "width": 171.6, + "height": 8 + }, + { + "width": 173.6, + "height": 9.6 + }, + { + "width": 176, + "height": 11.2 + }, + { + "width": 178, + "height": 12.8 + }, + { + "width": 180.4, + "height": 14.4 + }, + { + "width": 180.4, + "height": 14.4 + }, + { + "width": 182.8, + "height": 16.4 + }, + { + "width": 185.2, + "height": 18 + }, + { + "width": 187.2, + "height": 19.6 + }, + { + "width": 188, + "height": 24.8 + }, + { + "width": 188, + "height": 32.8 + }, + { + "width": 188, + "height": 44 + }, + { + "width": 188, + "height": 57.2 + }, + { + "width": 188, + "height": 70.8 + }, + { + "width": 188, + "height": 78 + }, + { + "width": 188, + "height": 91.2 + }, + { + "width": 188, + "height": 103.2 + }, + { + "width": 188, + "height": 114.4 + }, + { + "width": 188, + "height": 124.4 + }, + { + "width": 188, + "height": 134 + }, + { + "width": 188, + "height": 142.8 + }, + { + "width": 188, + "height": 150.8 + }, + { + "width": 188, + "height": 158.8 + }, + { + "width": 188, + "height": 159.6 + }, + { + "width": 188, + "height": 167.2 + }, + { + "width": 188, + "height": 174 + }, + { + "width": 188, + "height": 180.8 + }, + { + "width": 188, + "height": 187.6 + }, + { + "width": 188, + "height": 187.6 + }, + { + "width": 188, + "height": 207.2 + }, + { + "width": 188, + "height": 240 + }, + { + "width": 188, + "height": 275.2 + }, + { + "width": 188, + "height": 306.8 + }, + { + "width": 188, + "height": 333.2 + }, + { + "width": 188, + "height": 353.6 + }, + { + "width": 188, + "height": 368.8 + }, + { + "width": 188, + "height": 380 + }, + { + "width": 188, + "height": 387.6 + }, + { + "width": 188, + "height": 392.4 + }, + { + "width": 188, + "height": 395.6 + }, + { + "width": 188, + "height": 398 + }, + { + "width": 188, + "height": 398.8 + }, + { + "width": 188, + "height": 399.6 + }, + { + "width": 188, + "height": 400 + } + ] + }, + { + "name": "RevealElement_alpha", + "type": "float", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + 0, + 0, + 0, + 0, + 0.0067873597, + 0.0612576, + 0.19080025, + 0.39327443, + 0.5711931, + 0.70855826, + 0.8074064, + 0.8754226, + 0.9207788, + 0.95032376, + 0.9692185, + 0.98112255, + 0.9885286, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json new file mode 100644 index 000000000000..10a9ba7e2760 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json @@ -0,0 +1,424 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432, + 448, + 464, + 480, + 496, + 512, + 528, + 544, + 560, + 576, + 592, + 608, + 624 + ], + "features": [ + { + "name": "RevealElement_position", + "type": "dpOffset", + "data_points": [ + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50.4, + "y": 50.8 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 51.2, + "y": 50 + }, + { + "x": 55.6, + "y": 50 + }, + { + "x": 58.8, + "y": 50 + }, + { + "x": 60.8, + "y": 50 + }, + { + "x": 62, + "y": 50 + }, + { + "x": 63.2, + "y": 50 + }, + { + "x": 63.6, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + } + ] + }, + { + "name": "RevealElement_size", + "type": "dpSize", + "data_points": [ + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 389.6 + }, + { + "width": 188, + "height": 378.8 + }, + { + "width": 188, + "height": 366 + }, + { + "width": 188, + "height": 352 + }, + { + "width": 188, + "height": 352 + }, + { + "width": 188, + "height": 316.8 + }, + { + "width": 188, + "height": 261.2 + }, + { + "width": 188, + "height": 202.8 + }, + { + "width": 188, + "height": 150.8 + }, + { + "width": 188, + "height": 107.6 + }, + { + "width": 186, + "height": 74.4 + }, + { + "width": 177.2, + "height": 49.6 + }, + { + "width": 170.8, + "height": 39.6 + }, + { + "width": 166.8, + "height": 36.8 + }, + { + "width": 164, + "height": 32.4 + }, + { + "width": 162, + "height": 27.6 + }, + { + "width": 160.8, + "height": 22.8 + }, + { + "width": 160.4, + "height": 18.4 + }, + { + "width": 160, + "height": 14.4 + }, + { + "width": 160, + "height": 11.2 + }, + { + "width": 160, + "height": 8.4 + }, + { + "width": 160, + "height": 6 + }, + { + "width": 160, + "height": 4.4 + }, + { + "width": 160, + "height": 3.2 + }, + { + "width": 160, + "height": 2 + }, + { + "width": 160, + "height": 1.2 + }, + { + "width": 160, + "height": 0.8 + }, + { + "width": 160, + "height": 0.4 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 + } + ] + }, + { + "name": "RevealElement_alpha", + "type": "float", + "data_points": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0.9833227, + 0.8263634, + 0.623688, + 0.44261706, + 0.3016883, + 0.1997872, + 0.12944388, + 0.08242595, + 0.051743627, + 0.032093227, + 0.019698441, + 0.0119793415, + 0, + 0, + 0, + 0, + 0 + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json new file mode 100644 index 000000000000..d8bf48d32d20 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json @@ -0,0 +1,364 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432, + 448, + 464, + 480, + 496, + 512, + 528 + ], + "features": [ + { + "name": "RevealElement_position", + "type": "dpOffset", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "x": 62.4, + "y": 50 + }, + { + "x": 61.2, + "y": 50 + }, + { + "x": 59.2, + "y": 50 + }, + { + "x": 57.2, + "y": 50 + }, + { + "x": 54.8, + "y": 50 + }, + { + "x": 52.4, + "y": 50 + }, + { + "x": 52.4, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + } + ] + }, + { + "name": "RevealElement_size", + "type": "dpSize", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "width": 163.2, + "height": 2 + }, + { + "width": 166, + "height": 4.4 + }, + { + "width": 170, + "height": 6.8 + }, + { + "width": 174, + "height": 10 + }, + { + "width": 178.4, + "height": 13.2 + }, + { + "width": 183.6, + "height": 16.8 + }, + { + "width": 183.6, + "height": 16.8 + }, + { + "width": 188, + "height": 42.4 + }, + { + "width": 188, + "height": 100 + }, + { + "width": 188, + "height": 161.6 + }, + { + "width": 188, + "height": 218 + }, + { + "width": 188, + "height": 265.6 + }, + { + "width": 188, + "height": 303.6 + }, + { + "width": 188, + "height": 332.4 + }, + { + "width": 188, + "height": 354 + }, + { + "width": 188, + "height": 369.2 + }, + { + "width": 188, + "height": 380 + }, + { + "width": 188, + "height": 387.2 + }, + { + "width": 188, + "height": 392 + }, + { + "width": 188, + "height": 395.2 + }, + { + "width": 188, + "height": 397.6 + }, + { + "width": 188, + "height": 398.4 + }, + { + "width": 188, + "height": 398.8 + }, + { + "width": 188, + "height": 399.2 + }, + { + "width": 188, + "height": 399.6 + }, + { + "width": 188, + "height": 399.6 + } + ] + }, + { + "name": "RevealElement_alpha", + "type": "float", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + 0, + 0, + 0.008216977, + 0.06259775, + 0.19032806, + 0.39281356, + 0.57081985, + 0.7082821, + 0.80721295, + 0.8752918, + 0.9206928, + 0.95026827, + 0.9691833, + 0.98110056, + 0.988515, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json new file mode 100644 index 000000000000..57bdf3e1ecab --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json @@ -0,0 +1,744 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432, + 448, + 464, + 480, + 496, + 512, + 528, + 544, + 560, + 576, + 592, + 608, + 624, + 640, + 656, + 672, + 688, + 704, + 720, + 736, + 752, + 768, + 784, + 800, + 816, + 832, + 848, + 864, + 880, + 896, + 912, + 928, + 944, + 960, + 976, + 992, + 1008, + 1024, + 1040, + 1056, + 1072, + 1088, + 1104, + 1120, + 1136 + ], + "features": [ + { + "name": "RevealElement_position", + "type": "dpOffset", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "x": 62.8, + "y": 50 + }, + { + "x": 62, + "y": 50 + }, + { + "x": 61.2, + "y": 50 + }, + { + "x": 60.4, + "y": 50 + }, + { + "x": 59.6, + "y": 50 + }, + { + "x": 58.8, + "y": 50 + }, + { + "x": 58, + "y": 50 + }, + { + "x": 57.2, + "y": 50 + }, + { + "x": 56.4, + "y": 50 + }, + { + "x": 55.6, + "y": 50 + }, + { + "x": 55.2, + "y": 50 + }, + { + "x": 54.4, + "y": 50 + }, + { + "x": 53.6, + "y": 50 + }, + { + "x": 53.2, + "y": 50 + }, + { + "x": 52.8, + "y": 50 + }, + { + "x": 52, + "y": 50 + }, + { + "x": 51.6, + "y": 50 + }, + { + "x": 51.2, + "y": 50 + }, + { + "x": 50.8, + "y": 50 + }, + { + "x": 50.4, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50.4, + "y": 50 + }, + { + "x": 50.8, + "y": 50 + }, + { + "x": 51.2, + "y": 50 + }, + { + "x": 51.6, + "y": 50 + }, + { + "x": 52, + "y": 50 + }, + { + "x": 52.8, + "y": 50 + }, + { + "x": 53.2, + "y": 50 + }, + { + "x": 53.6, + "y": 50 + }, + { + "x": 54.4, + "y": 50 + }, + { + "x": 55.2, + "y": 50 + }, + { + "x": 55.6, + "y": 50 + }, + { + "x": 56.4, + "y": 50 + }, + { + "x": 57.2, + "y": 50 + }, + { + "x": 58, + "y": 50 + }, + { + "x": 58.8, + "y": 50 + }, + { + "x": 59.6, + "y": 50 + }, + { + "x": 60.4, + "y": 50 + }, + { + "x": 61.2, + "y": 50 + }, + { + "x": 62, + "y": 50 + }, + { + "x": 62.8, + "y": 50 + }, + { + "x": 63.6, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + } + ] + }, + { + "name": "RevealElement_size", + "type": "dpSize", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "width": 162.4, + "height": 1.6 + }, + { + "width": 164, + "height": 2.8 + }, + { + "width": 166, + "height": 4 + }, + { + "width": 167.6, + "height": 5.2 + }, + { + "width": 169.2, + "height": 6.4 + }, + { + "width": 170.8, + "height": 7.6 + }, + { + "width": 172.4, + "height": 8.8 + }, + { + "width": 174, + "height": 10 + }, + { + "width": 175.2, + "height": 10.8 + }, + { + "width": 176.8, + "height": 12 + }, + { + "width": 178, + "height": 12.8 + }, + { + "width": 179.2, + "height": 13.6 + }, + { + "width": 180.8, + "height": 14.8 + }, + { + "width": 182, + "height": 15.6 + }, + { + "width": 182.8, + "height": 16.4 + }, + { + "width": 184, + "height": 17.2 + }, + { + "width": 184.8, + "height": 17.6 + }, + { + "width": 186, + "height": 18.4 + }, + { + "width": 186.8, + "height": 19.2 + }, + { + "width": 187.6, + "height": 19.6 + }, + { + "width": 188, + "height": 21.2 + }, + { + "width": 188, + "height": 24 + }, + { + "width": 188, + "height": 29.6 + }, + { + "width": 188, + "height": 37.2 + }, + { + "width": 188, + "height": 45.6 + }, + { + "width": 188, + "height": 53.6 + }, + { + "width": 188, + "height": 60.4 + }, + { + "width": 188, + "height": 66.4 + }, + { + "width": 188, + "height": 71.2 + }, + { + "width": 188, + "height": 75.2 + }, + { + "width": 188, + "height": 77.6 + }, + { + "width": 188, + "height": 79.6 + }, + { + "width": 188, + "height": 80.4 + }, + { + "width": 188, + "height": 80.8 + }, + { + "width": 188, + "height": 80.4 + }, + { + "width": 188, + "height": 79.2 + }, + { + "width": 187.6, + "height": 78 + }, + { + "width": 186.8, + "height": 76 + }, + { + "width": 186, + "height": 74 + }, + { + "width": 184.8, + "height": 71.6 + }, + { + "width": 184, + "height": 69.2 + }, + { + "width": 182.8, + "height": 66 + }, + { + "width": 182, + "height": 62.8 + }, + { + "width": 180.8, + "height": 59.2 + }, + { + "width": 179.2, + "height": 55.6 + }, + { + "width": 178, + "height": 52 + }, + { + "width": 176.8, + "height": 48 + }, + { + "width": 175.2, + "height": 44 + }, + { + "width": 174, + "height": 40 + }, + { + "width": 172.4, + "height": 39.2 + }, + { + "width": 170.8, + "height": 38.4 + }, + { + "width": 169.2, + "height": 34.8 + }, + { + "width": 167.6, + "height": 30 + }, + { + "width": 166, + "height": 25.2 + }, + { + "width": 164, + "height": 20.4 + }, + { + "width": 162.4, + "height": 16.4 + }, + { + "width": 160.8, + "height": 12.8 + }, + { + "width": 160, + "height": 9.6 + }, + { + "width": 160, + "height": 7.2 + }, + { + "width": 160, + "height": 5.2 + }, + { + "width": 160, + "height": 3.6 + }, + { + "width": 160, + "height": 2.8 + }, + { + "width": 160, + "height": 1.6 + }, + { + "width": 160, + "height": 1.2 + }, + { + "width": 160, + "height": 0.8 + }, + { + "width": 160, + "height": 0.4 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 + } + ] + }, + { + "name": "RevealElement_alpha", + "type": "float", + "data_points": [ + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + { + "type": "not_found" + }, + 0, + 0, + 0, + 0, + 0.012518823, + 0.0741024, + 0.2254293, + 0.42628878, + 0.5976641, + 0.7280312, + 0.82100236, + 0.8845844, + 0.9267946, + 0.95419544, + 0.9716705, + 0.98265487, + 0.98947525, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0.9944124, + 0.9417388, + 0.8184184, + 0.6157812, + 0.4361611, + 0.2968906, + 0.19641554, + 0.12716137, + 0.080921985, + 0.050773025, + 0.03147719, + 0.019312752, + 0.011740655, + 0, + 0, + 0 + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json new file mode 100644 index 000000000000..9aa91c2d5e17 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json @@ -0,0 +1,304 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432 + ], + "features": [ + { + "name": "RevealElement_position", + "type": "dpOffset", + "data_points": [ + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 53.2, + "y": 50 + }, + { + "x": 57.2, + "y": 50 + }, + { + "x": 59.6, + "y": 50 + }, + { + "x": 61.6, + "y": 50 + }, + { + "x": 62.8, + "y": 50 + }, + { + "x": 63.6, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + }, + { + "x": 64, + "y": 50 + } + ] + }, + { + "name": "RevealElement_size", + "type": "dpSize", + "data_points": [ + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 400 + }, + { + "width": 188, + "height": 372 + }, + { + "width": 188, + "height": 312.8 + }, + { + "width": 188, + "height": 246.8 + }, + { + "width": 188, + "height": 185.2 + }, + { + "width": 188, + "height": 133.6 + }, + { + "width": 188, + "height": 93.2 + }, + { + "width": 181.6, + "height": 62.8 + }, + { + "width": 174, + "height": 40.8 + }, + { + "width": 168.8, + "height": 38.4 + }, + { + "width": 165.2, + "height": 34.8 + }, + { + "width": 162.8, + "height": 30 + }, + { + "width": 161.2, + "height": 25.2 + }, + { + "width": 160.4, + "height": 20.4 + }, + { + "width": 160, + "height": 16.4 + }, + { + "width": 160, + "height": 12.8 + }, + { + "width": 160, + "height": 9.6 + }, + { + "width": 160, + "height": 7.2 + }, + { + "width": 160, + "height": 5.2 + }, + { + "width": 160, + "height": 3.6 + }, + { + "width": 160, + "height": 2.8 + }, + { + "width": 160, + "height": 1.6 + }, + { + "width": 160, + "height": 1.2 + }, + { + "width": 160, + "height": 0.8 + }, + { + "width": 160, + "height": 0.4 + }, + { + "width": 160, + "height": 0 + }, + { + "width": 160, + "height": 0 + } + ] + }, + { + "name": "RevealElement_alpha", + "type": "float", + "data_points": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0.91758585, + 0.72435355, + 0.52812576, + 0.3665868, + 0.24600428, + 0.16102076, + 0.103373945, + 0.06533456, + 0.04075712, + 0.025142312, + 0.015358448, + 0.0092999935, + 0, + 0, + 0, + 0, + 0 + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealOpenTransition.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealOpenTransition.json new file mode 100644 index 000000000000..622c29eebfb4 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealOpenTransition.json @@ -0,0 +1,244 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336 + ], + "features": [ + { + "name": "RevealElement_position", + "type": "dpOffset", + "data_points": [ + { + "type": "not_found" + }, + { + "x": 64, + "y": 50 + }, + { + "x": 59.2, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": 50, + "y": 50 + } + ] + }, + { + "name": "RevealElement_size", + "type": "dpSize", + "data_points": [ + { + "type": "not_found" + }, + { + "width": 160, + "height": 0 + }, + { + "width": 169.6, + "height": 6.8 + }, + { + "width": 188, + "height": 26.8 + }, + { + "width": 188, + "height": 95.6 + }, + { + "width": 188, + "height": 163.2 + }, + { + "width": 188, + "height": 222 + }, + { + "width": 188, + "height": 269.6 + }, + { + "width": 188, + "height": 307.2 + }, + { + "width": 188, + "height": 335.2 + }, + { + "width": 188, + "height": 356 + }, + { + "width": 188, + "height": 370.4 + }, + { + "width": 188, + "height": 380.8 + }, + { + "width": 188, + "height": 387.6 + }, + { + "width": 188, + "height": 392.4 + }, + { + "width": 188, + "height": 395.2 + }, + { + "width": 188, + "height": 397.2 + }, + { + "width": 188, + "height": 398 + }, + { + "width": 188, + "height": 398.8 + }, + { + "width": 188, + "height": 399.2 + }, + { + "width": 188, + "height": 399.2 + }, + { + "width": 188, + "height": 399.6 + } + ] + }, + { + "name": "RevealElement_alpha", + "type": "float", + "data_points": [ + { + "type": "not_found" + }, + 0, + 0.05698657, + 0.24197984, + 0.44158113, + 0.6097554, + 0.73685503, + 0.8271309, + 0.8886989, + 0.9294886, + 0.9559254, + 0.97276413, + 0.98333716, + 0.98989624, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/mechanics/TransitionScopedMechanicsAdapterTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/mechanics/TransitionScopedMechanicsAdapterTest.kt index b9bd115782b7..9d403507252f 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/mechanics/TransitionScopedMechanicsAdapterTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/mechanics/TransitionScopedMechanicsAdapterTest.kt @@ -70,7 +70,7 @@ import org.junit.Test import org.junit.runner.RunWith import platform.test.motion.compose.ComposeRecordingSpec import platform.test.motion.compose.MotionControl -import platform.test.motion.compose.createComposeMotionTestRule +import platform.test.motion.compose.createFixedConfigurationComposeMotionTestRule import platform.test.motion.compose.recordMotion import platform.test.motion.compose.runTest import platform.test.motion.golden.DataPoint @@ -86,7 +86,7 @@ class TransitionScopedMechanicsAdapterTest { createGoldenPathManager("frameworks/base/packages/SystemUI/compose/scene/tests/goldens") private val testScope = TestScope() - @get:Rule val motionRule = createComposeMotionTestRule(goldenPaths, testScope) + @get:Rule val motionRule = createFixedConfigurationComposeMotionTestRule(goldenPaths, testScope) private val composeRule = motionRule.toolkit.composeContentTestRule @Test diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/reveal/ContentRevealTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/reveal/ContentRevealTest.kt new file mode 100644 index 000000000000..f4e2328f16ab --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/reveal/ContentRevealTest.kt @@ -0,0 +1,279 @@ +/* + * 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.compose.animation.scene.reveal + +import android.platform.test.annotations.MotionTest +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.TouchInjectionScope +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.swipe +import androidx.compose.ui.test.swipeDown +import androidx.compose.ui.test.swipeUp +import androidx.compose.ui.test.swipeWithVelocity +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.FeatureCaptures.elementAlpha +import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateForTests +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayoutForTesting +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.featureOfElement +import com.android.compose.animation.scene.transitions +import com.android.mechanics.behavior.EdgeContainerExpansionSpec +import com.android.mechanics.behavior.edgeContainerExpansionBackground +import kotlin.math.sin +import kotlinx.coroutines.CoroutineScope +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.motion.compose.ComposeFeatureCaptures.dpSize +import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot +import platform.test.motion.compose.ComposeRecordingSpec +import platform.test.motion.compose.MotionControl +import platform.test.motion.compose.MotionControlScope +import platform.test.motion.compose.createFixedConfigurationComposeMotionTestRule +import platform.test.motion.compose.recordMotion +import platform.test.motion.compose.runTest +import platform.test.motion.testing.createGoldenPathManager + +@RunWith(AndroidJUnit4::class) +@MotionTest +class ContentRevealTest { + + private val goldenPaths = + createGoldenPathManager("frameworks/base/packages/SystemUI/compose/scene/tests/goldens") + + @get:Rule val motionRule = createFixedConfigurationComposeMotionTestRule(goldenPaths) + + private val fakeHaptics = FakeHaptics() + + @Test + fun verticalReveal_triggeredRevealOpenTransition() { + assertVerticalContainerRevealMotion(TriggeredRevealMotion(SceneClosed, SceneOpen)) + } + + @Test + fun verticalReveal_triggeredRevealCloseTransition() { + assertVerticalContainerRevealMotion(TriggeredRevealMotion(SceneOpen, SceneClosed)) + } + + @Test + fun verticalReveal_gesture_magneticDetachAndReattach() { + assertVerticalContainerRevealMotion( + GestureRevealMotion(SceneClosed) { + val gestureDurationMillis = 1000L + swipe( + curve = { + val progress = it / gestureDurationMillis.toFloat() + val y = sin(progress * Math.PI).toFloat() * 100.dp.toPx() + Offset(centerX, y) + }, + gestureDurationMillis, + ) + } + ) + } + + @Test + fun verticalReveal_gesture_dragOpen() { + assertVerticalContainerRevealMotion( + GestureRevealMotion(SceneClosed) { + swipeDown(endY = 200.dp.toPx(), durationMillis = 500) + } + ) + } + + @Test + fun verticalReveal_gesture_flingOpen() { + assertVerticalContainerRevealMotion( + GestureRevealMotion(SceneClosed) { + val end = Offset(centerX, 80.dp.toPx()) + swipeWithVelocity(start = topCenter, end = end, endVelocity = FlingVelocity.toPx()) + } + ) + } + + @Test + fun verticalReveal_gesture_dragFullyClose() { + assertVerticalContainerRevealMotion( + GestureRevealMotion(SceneOpen) { + swipeUp(200.dp.toPx(), 0.dp.toPx(), durationMillis = 500) + } + ) + } + + @Test + fun verticalReveal_gesture_dragHalfClose() { + assertVerticalContainerRevealMotion( + GestureRevealMotion(SceneOpen) { + swipeUp(350.dp.toPx(), 100.dp.toPx(), durationMillis = 500) + } + ) + } + + @Test + fun verticalReveal_gesture_flingClose() { + assertVerticalContainerRevealMotion( + GestureRevealMotion(SceneOpen) { + val start = Offset(centerX, 260.dp.toPx()) + val end = Offset(centerX, 200.dp.toPx()) + swipeWithVelocity(start, end, FlingVelocity.toPx()) + } + ) + } + + private interface RevealMotion { + val startScene: SceneKey + } + + private class TriggeredRevealMotion( + override val startScene: SceneKey, + val targetScene: SceneKey, + ) : RevealMotion + + private class GestureRevealMotion( + override val startScene: SceneKey, + val gestureControl: TouchInjectionScope.() -> Unit, + ) : RevealMotion + + private fun assertVerticalContainerRevealMotion(testInstructions: RevealMotion) = + motionRule.runTest { + val transitions = transitions { + from(SceneClosed, to = SceneOpen) { + verticalContainerReveal(RevealElement, MotionSpec, fakeHaptics) + } + } + + val state = + toolkit.composeContentTestRule.runOnUiThread { + MutableSceneTransitionLayoutStateForTests( + testInstructions.startScene, + transitions, + ) + } + lateinit var coroutineScope: CoroutineScope + + val recordTransition: suspend MotionControlScope.() -> Unit = { + when (testInstructions) { + is TriggeredRevealMotion -> { + val transition = + toolkit.composeContentTestRule.runOnUiThread { + state.setTargetScene( + testInstructions.targetScene, + animationScope = coroutineScope, + ) + } + checkNotNull(transition).second.join() + } + + is GestureRevealMotion -> { + performTouchInputAsync( + onNodeWithTag("stl"), + testInstructions.gestureControl, + ) + awaitCondition { !state.isTransitioning() } + } + } + } + val recordingSpec = + ComposeRecordingSpec( + recordBefore = false, + recordAfter = false, + motionControl = MotionControl(recording = recordTransition), + ) { + featureOfElement(RevealElement, positionInRoot) + featureOfElement(RevealElement, dpSize) + featureOfElement(RevealElement, elementAlpha) + } + + val motion = + recordMotion( + content = { + coroutineScope = rememberCoroutineScope() + SceneTransitionLayoutForTesting( + state, + modifier = + Modifier.padding(50.dp) + .background(Color.Yellow) + .size(ContainerSize.width, ContainerSize.height + 200.dp) + .testTag("stl"), + ) { + scene( + SceneClosed, + mapOf(Swipe.Down to SceneOpen), + content = { ClosedContainer() }, + ) + scene( + SceneOpen, + mapOf(Swipe.Up to SceneClosed), + content = { OpenContainer() }, + ) + } + }, + recordingSpec, + ) + + assertThat(motion).timeSeriesMatchesGolden() + } + + @Composable + fun ContentScope.ClosedContainer() { + Box(modifier = Modifier.fillMaxSize()) + } + + @Composable + fun ContentScope.OpenContainer() { + Box(contentAlignment = Alignment.TopCenter, modifier = Modifier.fillMaxSize()) { + Box( + modifier = + Modifier.element(RevealElement) + .size(ContainerSize) + .edgeContainerExpansionBackground(Color.DarkGray, MotionSpec) + ) + } + } + + private class FakeHaptics : ContainerRevealHaptics { + override fun onRevealThresholdCrossed(revealed: Boolean) {} + } + + companion object { + val ContainerSize = DpSize(200.dp, 400.dp) + + val FlingVelocity = 1000.dp // dp/sec + + val SceneClosed = SceneKey("SceneA") + val SceneOpen = SceneKey("SceneB") + + val RevealElement = ElementKey("RevealElement") + val MotionSpec = EdgeContainerExpansionSpec() + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt index ea6f208d6bb9..3b008ac4c1df 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt @@ -31,27 +31,21 @@ import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.TransitionRecordingSpec import com.android.compose.animation.scene.featureOfElement import com.android.compose.animation.scene.recordTransition -import org.junit.ClassRule import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import platform.test.motion.compose.ComposeFeatureCaptures -import platform.test.motion.compose.createComposeMotionTestRule +import platform.test.motion.compose.createFixedConfigurationComposeMotionTestRule import platform.test.motion.testing.createGoldenPathManager -import platform.test.screenshot.ResetDeviceEmulationRule @RunWith(AndroidJUnit4::class) @MotionTest class AnchoredSizeTest { - companion object { - @JvmField @ClassRule val cleanupRule: ResetDeviceEmulationRule = ResetDeviceEmulationRule() - } - private val goldenPaths = createGoldenPathManager("frameworks/base/packages/SystemUI/compose/scene/tests/goldens") - @get:Rule val motionRule = createComposeMotionTestRule(goldenPaths) + @get:Rule val motionRule = createFixedConfigurationComposeMotionTestRule(goldenPaths) @Test fun testAnchoredSizeEnter() { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index 9bb3bac824e9..365567b17ec0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -17,7 +17,6 @@ import android.content.Context import android.content.res.Resources import android.graphics.Color import android.graphics.Rect -import android.graphics.RectF import android.icu.text.NumberFormat import android.util.TypedValue import android.view.LayoutInflater @@ -29,6 +28,7 @@ import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockConfig import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.ClockEventListener import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController @@ -102,7 +102,7 @@ class DefaultClockController( isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float, - onBoundsChanged: (RectF) -> Unit, + clockListener: ClockEventListener?, ) { largeClock.recomputePadding(null) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index 6dfd2268005f..5acd4468fe92 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -16,13 +16,13 @@ package com.android.systemui.shared.clocks -import android.graphics.RectF import com.android.systemui.animation.GSFAxes import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.AxisType import com.android.systemui.plugins.clocks.ClockConfig import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.ClockEventListener import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFontAxis import com.android.systemui.plugins.clocks.ClockFontAxis.Companion.merge @@ -107,11 +107,11 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float, - onBoundsChanged: (RectF) -> Unit, + clockListener: ClockEventListener?, ) { events.onFontAxesChanged(clockCtx.settings.axes) smallClock.run { - layerController.onViewBoundsChanged = onBoundsChanged + layerController.onViewBoundsChanged = { clockListener?.onBoundsChanged(it) } events.onThemeChanged(theme.copy(isDarkTheme = isDarkTheme)) animations.doze(dozeFraction) animations.fold(foldFraction) @@ -119,7 +119,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController } largeClock.run { - layerController.onViewBoundsChanged = onBoundsChanged + layerController.onViewBoundsChanged = { clockListener?.onBoundsChanged(it) } events.onThemeChanged(theme.copy(isDarkTheme = isDarkTheme)) animations.doze(dozeFraction) animations.fold(foldFraction) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index aeea99be40dd..a2f5a30a20ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -193,8 +193,6 @@ public class UdfpsControllerTest extends SysuiTestCase { @Mock private UdfpsOverlayInteractor mUdfpsOverlayInteractor; @Mock - private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate; - @Mock private SelectedUserInteractor mSelectedUserInteractor; // Capture listeners so that they can be used to send events @@ -321,7 +319,6 @@ public class UdfpsControllerTest extends SysuiTestCase { mAlternateBouncerInteractor, mInputManager, mock(DeviceEntryFaceAuthInteractor.class), - mUdfpsKeyguardAccessibilityDelegate, mSelectedUserInteractor, mKeyguardTransitionInteractor, mDeviceEntryUdfpsTouchOverlayViewModel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt deleted file mode 100644 index 921ff098753e..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegateTest.kt +++ /dev/null @@ -1,86 +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.systemui.biometrics - -import android.testing.TestableLooper -import android.view.View -import android.view.accessibility.AccessibilityNodeInfo -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager -import com.android.systemui.util.mockito.argumentCaptor -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.Mock -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations - -@RunWith(AndroidJUnit4::class) -@SmallTest -@TestableLooper.RunWithLooper -class UdfpsKeyguardAccessibilityDelegateTest : SysuiTestCase() { - - @Mock private lateinit var keyguardViewManager: StatusBarKeyguardViewManager - @Mock private lateinit var hostView: View - private lateinit var underTest: UdfpsKeyguardAccessibilityDelegate - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - underTest = - UdfpsKeyguardAccessibilityDelegate( - context.resources, - keyguardViewManager, - ) - } - - @Test - fun onInitializeAccessibilityNodeInfo_clickActionAdded() { - // WHEN node is initialized - val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java) - underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo) - - // THEN a11y action is added - val argumentCaptor = argumentCaptor<AccessibilityNodeInfo.AccessibilityAction>() - verify(mockedNodeInfo).addAction(argumentCaptor.capture()) - - // AND the a11y action is a click action - assertEquals( - AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, - argumentCaptor.value.id - ) - } - - @Test - fun performAccessibilityAction_actionClick_showsPrimaryBouncer() { - // WHEN click action is performed - val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java) - underTest.performAccessibilityAction( - hostView, - AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, - null - ) - - // THEN primary bouncer shows - verify(keyguardViewManager).showPrimaryBouncer(anyBoolean()) - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.kt new file mode 100644 index 000000000000..239e02640908 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ActionIntentCreatorTest.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.clipboardoverlay + +import android.content.ClipData +import android.content.ComponentName +import android.content.Intent +import android.net.Uri +import android.text.SpannableString +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ActionIntentCreatorTest : SysuiTestCase() { + val creator = ActionIntentCreator() + + @Test + fun test_getTextEditorIntent() { + val intent = creator.getTextEditorIntent(context) + assertEquals(ComponentName(context, EditTextActivity::class.java), intent.component) + assertFlags(intent, Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + + @Test + fun test_getRemoteCopyIntent() { + context.getOrCreateTestableResources().addOverride(R.string.config_remoteCopyPackage, "") + + val clipData = ClipData.newPlainText("Test", "Test Item") + var intent = creator.getRemoteCopyIntent(clipData, context) + + assertEquals(null, intent.component) + assertFlags(intent, EXTERNAL_INTENT_FLAGS) + assertEquals(clipData, intent.clipData) + + // Try again with a remote copy component + val fakeComponent = + ComponentName("com.android.remotecopy", "com.android.remotecopy.RemoteCopyActivity") + context + .getOrCreateTestableResources() + .addOverride(R.string.config_remoteCopyPackage, fakeComponent.flattenToString()) + + intent = creator.getRemoteCopyIntent(clipData, context) + assertEquals(fakeComponent, intent.component) + } + + @Test + fun test_getImageEditIntent() { + context.getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, "") + val fakeUri = Uri.parse("content://foo") + var intent = creator.getImageEditIntent(fakeUri, context) + + assertEquals(Intent.ACTION_EDIT, intent.action) + assertEquals("image/*", intent.type) + assertEquals(null, intent.component) + assertEquals("clipboard", intent.getStringExtra("edit_source")) + assertFlags(intent, EXTERNAL_INTENT_FLAGS) + + // try again with an editor component + val fakeComponent = + ComponentName("com.android.remotecopy", "com.android.remotecopy.RemoteCopyActivity") + context + .getOrCreateTestableResources() + .addOverride(R.string.config_screenshotEditor, fakeComponent.flattenToString()) + intent = creator.getImageEditIntent(fakeUri, context) + assertEquals(fakeComponent, intent.component) + } + + @Test + fun test_getShareIntent_plaintext() { + val clipData = ClipData.newPlainText("Test", "Test Item") + val intent = creator.getShareIntent(clipData, context) + + assertEquals(Intent.ACTION_CHOOSER, intent.action) + assertFlags(intent, EXTERNAL_INTENT_FLAGS) + val target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + assertEquals("Test Item", target?.getStringExtra(Intent.EXTRA_TEXT)) + assertEquals("text/plain", target?.type) + } + + @Test + fun test_getShareIntent_html() { + val clipData = ClipData.newHtmlText("Test", "Some HTML", "<b>Some HTML</b>") + val intent = creator.getShareIntent(clipData, getContext()) + + assertEquals(Intent.ACTION_CHOOSER, intent.action) + assertFlags(intent, EXTERNAL_INTENT_FLAGS) + val target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + assertEquals("Some HTML", target?.getStringExtra(Intent.EXTRA_TEXT)) + assertEquals("text/plain", target?.type) + } + + @Test + fun test_getShareIntent_image() { + val uri = Uri.parse("content://something") + val clipData = ClipData("Test", arrayOf("image/png"), ClipData.Item(uri)) + val intent = creator.getShareIntent(clipData, context) + + assertEquals(Intent.ACTION_CHOOSER, intent.action) + assertFlags(intent, EXTERNAL_INTENT_FLAGS) + val target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + assertEquals(uri, target?.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)) + assertEquals(uri, target?.clipData?.getItemAt(0)?.uri) + assertEquals("image/png", target?.type) + } + + @Test + fun test_getShareIntent_spannableText() { + val clipData = ClipData.newPlainText("Test", SpannableString("Test Item")) + val intent = creator.getShareIntent(clipData, context) + + assertEquals(Intent.ACTION_CHOOSER, intent.action) + assertFlags(intent, EXTERNAL_INTENT_FLAGS) + val target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) + assertEquals("Test Item", target?.getStringExtra(Intent.EXTRA_TEXT)) + assertEquals("text/plain", target?.type) + } + + // Assert that the given flags are set + private fun assertFlags(intent: Intent, flags: Int) { + assertTrue((intent.flags and flags) == flags) + } + + companion object { + private const val EXTERNAL_INTENT_FLAGS: Int = + (Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_GRANT_READ_URI_PERMISSION) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/DefaultIntentCreatorTest.java index ea6cb3b6d178..126b3fa9e7ca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/DefaultIntentCreatorTest.java @@ -36,13 +36,15 @@ import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) -public class IntentCreatorTest extends SysuiTestCase { +public class DefaultIntentCreatorTest extends SysuiTestCase { private static final int EXTERNAL_INTENT_FLAGS = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION; + private final DefaultIntentCreator mIntentCreator = new DefaultIntentCreator(); + @Test public void test_getTextEditorIntent() { - Intent intent = IntentCreator.getTextEditorIntent(getContext()); + Intent intent = mIntentCreator.getTextEditorIntent(getContext()); assertEquals(new ComponentName(getContext(), EditTextActivity.class), intent.getComponent()); assertFlags(intent, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -54,7 +56,7 @@ public class IntentCreatorTest extends SysuiTestCase { ""); ClipData clipData = ClipData.newPlainText("Test", "Test Item"); - Intent intent = IntentCreator.getRemoteCopyIntent(clipData, getContext()); + Intent intent = mIntentCreator.getRemoteCopyIntent(clipData, getContext()); assertEquals(null, intent.getComponent()); assertFlags(intent, EXTERNAL_INTENT_FLAGS); @@ -66,7 +68,7 @@ public class IntentCreatorTest extends SysuiTestCase { getContext().getOrCreateTestableResources().addOverride(R.string.config_remoteCopyPackage, fakeComponent.flattenToString()); - intent = IntentCreator.getRemoteCopyIntent(clipData, getContext()); + intent = mIntentCreator.getRemoteCopyIntent(clipData, getContext()); assertEquals(fakeComponent, intent.getComponent()); } @@ -75,7 +77,7 @@ public class IntentCreatorTest extends SysuiTestCase { getContext().getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, ""); Uri fakeUri = Uri.parse("content://foo"); - Intent intent = IntentCreator.getImageEditIntent(fakeUri, getContext()); + Intent intent = mIntentCreator.getImageEditIntent(fakeUri, getContext()); assertEquals(Intent.ACTION_EDIT, intent.getAction()); assertEquals("image/*", intent.getType()); @@ -88,14 +90,14 @@ public class IntentCreatorTest extends SysuiTestCase { "com.android.remotecopy.RemoteCopyActivity"); getContext().getOrCreateTestableResources().addOverride(R.string.config_screenshotEditor, fakeComponent.flattenToString()); - intent = IntentCreator.getImageEditIntent(fakeUri, getContext()); + intent = mIntentCreator.getImageEditIntent(fakeUri, getContext()); assertEquals(fakeComponent, intent.getComponent()); } @Test public void test_getShareIntent_plaintext() { ClipData clipData = ClipData.newPlainText("Test", "Test Item"); - Intent intent = IntentCreator.getShareIntent(clipData, getContext()); + Intent intent = mIntentCreator.getShareIntent(clipData, getContext()); assertEquals(Intent.ACTION_CHOOSER, intent.getAction()); assertFlags(intent, EXTERNAL_INTENT_FLAGS); @@ -108,7 +110,7 @@ public class IntentCreatorTest extends SysuiTestCase { public void test_getShareIntent_html() { ClipData clipData = ClipData.newHtmlText("Test", "Some HTML", "<b>Some HTML</b>"); - Intent intent = IntentCreator.getShareIntent(clipData, getContext()); + Intent intent = mIntentCreator.getShareIntent(clipData, getContext()); assertEquals(Intent.ACTION_CHOOSER, intent.getAction()); assertFlags(intent, EXTERNAL_INTENT_FLAGS); @@ -122,7 +124,7 @@ public class IntentCreatorTest extends SysuiTestCase { Uri uri = Uri.parse("content://something"); ClipData clipData = new ClipData("Test", new String[]{"image/png"}, new ClipData.Item(uri)); - Intent intent = IntentCreator.getShareIntent(clipData, getContext()); + Intent intent = mIntentCreator.getShareIntent(clipData, getContext()); assertEquals(Intent.ACTION_CHOOSER, intent.getAction()); assertFlags(intent, EXTERNAL_INTENT_FLAGS); @@ -135,7 +137,7 @@ public class IntentCreatorTest extends SysuiTestCase { @Test public void test_getShareIntent_spannableText() { ClipData clipData = ClipData.newPlainText("Test", new SpannableString("Test Item")); - Intent intent = IntentCreator.getShareIntent(clipData, getContext()); + Intent intent = mIntentCreator.getShareIntent(clipData, getContext()); assertEquals(Intent.ACTION_CHOOSER, intent.getAction()); assertFlags(intent, EXTERNAL_INTENT_FLAGS); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/SysUIStatePerDisplayInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/SysUIStatePerDisplayInteractorTest.kt index ed9cd98a825a..f64f13d4a9b5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/SysUIStatePerDisplayInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/domain/interactor/SysUIStatePerDisplayInteractorTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.common.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.model.StateChange import com.android.systemui.model.fakeSysUIStatePerDisplayRepository import com.android.systemui.model.sysUiStateFactory @@ -26,6 +27,7 @@ import com.android.systemui.model.sysuiStateInteractor import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.runner.RunWith @@ -49,6 +51,13 @@ class SysUIStatePerDisplayInteractorTest : SysuiTestCase() { add(1, state1) add(2, state2) } + runBlocking { + kosmos.displayRepository.apply { + addDisplay(0) + addDisplay(1) + addDisplay(2) + } + } } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt index 299105e2dabd..e41d46ce90af 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayInstanceRepositoryImplTest.kt @@ -20,6 +20,7 @@ import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.testKosmos @@ -29,6 +30,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyString +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) @SmallTest @@ -99,6 +103,11 @@ class PerDisplayInstanceRepositoryImplTest : SysuiTestCase() { assertThat(fakePerDisplayInstanceProviderWithTeardown.destroyed).isEmpty() } + @Test + fun start_registersDumpable() { + verify(kosmos.dumpManager).registerNormalDumpable(anyString(), eq(underTest)) + } + private fun createDisplay(displayId: Int): Display = display(type = Display.TYPE_INTERNAL, id = displayId) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt index 0197a1e61801..c72afc72fa16 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt @@ -16,26 +16,17 @@ package com.android.systemui.media.controls.domain.interactor -import android.R -import android.graphics.drawable.Icon import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope -import com.android.systemui.media.controls.MediaTestHelper import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor -import com.android.systemui.media.controls.domain.pipeline.interactor.MediaRecommendationsInteractor import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor -import com.android.systemui.media.controls.domain.pipeline.interactor.mediaRecommendationsInteractor -import com.android.systemui.media.controls.shared.model.MediaCommonModel import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData -import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -52,16 +43,6 @@ class MediaCarouselInteractorTest : SysuiTestCase() { private val mediaFilterRepository: MediaFilterRepository = with(kosmos) { mediaFilterRepository } - private val mediaRecommendationsInteractor: MediaRecommendationsInteractor = - kosmos.mediaRecommendationsInteractor - val icon = Icon.createWithResource(context, R.drawable.ic_media_play) - private val mediaRecommendation = - SmartspaceMediaData( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - packageName = PACKAGE_NAME, - recommendations = MediaTestHelper.getValidRecommendationList(icon), - ) private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor @@ -119,81 +100,6 @@ class MediaCarouselInteractorTest : SysuiTestCase() { } @Test - fun addActiveRecommendation_inactiveMedia() = - testScope.runTest { - val hasActiveMediaOrRecommendation by - collectLastValue(underTest.hasActiveMediaOrRecommendation) - val hasAnyMediaOrRecommendation by - collectLastValue(underTest.hasAnyMediaOrRecommendation) - val currentMedia by collectLastValue(underTest.currentMedia) - - val userMedia = MediaData(active = false) - val recsLoadingModel = SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true) - val mediaLoadingModel = MediaDataLoadingModel.Loaded(userMedia.instanceId) - - mediaFilterRepository.setRecommendation(mediaRecommendation) - mediaFilterRepository.setRecommendationsLoadingState(recsLoadingModel) - - assertThat(hasActiveMediaOrRecommendation).isTrue() - assertThat(hasAnyMediaOrRecommendation).isTrue() - assertThat(currentMedia) - .containsExactly(MediaCommonModel.MediaRecommendations(recsLoadingModel)) - - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(mediaLoadingModel) - mediaFilterRepository.setOrderedMedia() - - assertThat(hasActiveMediaOrRecommendation).isTrue() - assertThat(hasAnyMediaOrRecommendation).isTrue() - assertThat(currentMedia) - .containsExactly( - MediaCommonModel.MediaRecommendations(recsLoadingModel), - MediaCommonModel.MediaControl(mediaLoadingModel, true), - ) - .inOrder() - } - - @Test - fun addActiveRecommendation_thenInactive() = - testScope.runTest { - val hasActiveMediaOrRecommendation by - collectLastValue(underTest.hasActiveMediaOrRecommendation) - val hasAnyMediaOrRecommendation by - collectLastValue(underTest.hasAnyMediaOrRecommendation) - - mediaFilterRepository.setRecommendation(mediaRecommendation) - - assertThat(hasActiveMediaOrRecommendation).isTrue() - assertThat(hasAnyMediaOrRecommendation).isTrue() - - mediaFilterRepository.setRecommendation(mediaRecommendation.copy(isActive = false)) - - assertThat(hasActiveMediaOrRecommendation).isFalse() - assertThat(hasAnyMediaOrRecommendation).isFalse() - } - - @Test - fun addActiveRecommendation_thenInvalid() = - testScope.runTest { - val hasActiveMediaOrRecommendation by - collectLastValue(underTest.hasActiveMediaOrRecommendation) - val hasAnyMediaOrRecommendation by - collectLastValue(underTest.hasAnyMediaOrRecommendation) - - mediaFilterRepository.setRecommendation(mediaRecommendation) - - assertThat(hasActiveMediaOrRecommendation).isTrue() - assertThat(hasAnyMediaOrRecommendation).isTrue() - - mediaFilterRepository.setRecommendation( - mediaRecommendation.copy(recommendations = listOf()) - ) - - assertThat(hasActiveMediaOrRecommendation).isFalse() - assertThat(hasAnyMediaOrRecommendation).isFalse() - } - - @Test fun hasAnyMedia_noMediaSet_returnsFalse() = testScope.runTest { assertThat(underTest.hasAnyMedia()).isFalse() } @@ -208,47 +114,4 @@ class MediaCarouselInteractorTest : SysuiTestCase() { @Test fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() = testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() } - - @Test - fun loadMediaFromRec() = - testScope.runTest { - val currentMedia by collectLastValue(underTest.currentMedia) - val instanceId = InstanceId.fakeInstanceId(123) - val data = - MediaData( - active = true, - instanceId = instanceId, - packageName = PACKAGE_NAME, - notificationKey = KEY, - ) - val smartspaceLoadingModel = SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE) - val mediaLoadingModel = MediaDataLoadingModel.Loaded(instanceId) - - mediaFilterRepository.setRecommendation(mediaRecommendation) - mediaFilterRepository.setRecommendationsLoadingState(smartspaceLoadingModel) - mediaRecommendationsInteractor.switchToMediaControl(PACKAGE_NAME) - mediaFilterRepository.addSelectedUserMediaEntry(data) - mediaFilterRepository.addMediaDataLoadingState(mediaLoadingModel) - - assertThat(currentMedia) - .containsExactly(MediaCommonModel.MediaRecommendations(smartspaceLoadingModel)) - .inOrder() - - mediaFilterRepository.addSelectedUserMediaEntry(data.copy(isPlaying = true)) - mediaFilterRepository.addMediaDataLoadingState(mediaLoadingModel) - - assertThat(currentMedia) - .containsExactly( - MediaCommonModel.MediaControl(mediaLoadingModel, isMediaFromRec = true), - MediaCommonModel.MediaRecommendations(smartspaceLoadingModel), - ) - .inOrder() - } - - companion object { - private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" - private const val PACKAGE_NAME = "com.android.example" - private const val KEY = "key" - private const val SURFACE = 4 - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaRecommendationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaRecommendationsInteractorTest.kt deleted file mode 100644 index 2265c0149cc3..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaRecommendationsInteractorTest.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.domain.interactor - -import android.R -import android.content.ComponentName -import android.content.Intent -import android.content.applicationContext -import android.graphics.drawable.Icon -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.broadcast.broadcastSender -import com.android.systemui.broadcast.mockBroadcastSender -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testScope -import com.android.systemui.media.controls.MediaTestHelper -import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl -import com.android.systemui.media.controls.domain.pipeline.interactor.MediaRecommendationsInteractor -import com.android.systemui.media.controls.domain.pipeline.interactor.MediaRecommendationsInteractor.Companion.EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME -import com.android.systemui.media.controls.domain.pipeline.interactor.mediaRecommendationsInteractor -import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter -import com.android.systemui.media.controls.shared.model.MediaRecModel -import com.android.systemui.media.controls.shared.model.MediaRecommendationsModel -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData -import com.android.systemui.plugins.activityStarter -import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.doNothing -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify -import org.mockito.kotlin.eq - -@SmallTest -@RunWith(AndroidJUnit4::class) -class MediaRecommendationsInteractorTest : SysuiTestCase() { - - private val spyContext = spy(context) - private val kosmos = testKosmos().apply { applicationContext = spyContext } - private val testScope = kosmos.testScope - - private val mediaDataFilter: MediaDataFilterImpl = with(kosmos) { mediaDataFilter } - private val activityStarter = kosmos.activityStarter - private val icon: Icon = Icon.createWithResource(context, R.drawable.ic_media_play) - private val smartspaceMediaData: SmartspaceMediaData = - SmartspaceMediaData( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - packageName = PACKAGE_NAME, - recommendations = MediaTestHelper.getValidRecommendationList(icon), - ) - - private val underTest: MediaRecommendationsInteractor = - with(kosmos) { - broadcastSender = mockBroadcastSender - kosmos.mediaRecommendationsInteractor - } - - @Test - fun addRecommendation_smartspaceMediaDataUpdate() = - testScope.runTest { - val recommendations by collectLastValue(underTest.recommendations) - - val model = - MediaRecommendationsModel( - key = KEY_MEDIA_SMARTSPACE, - packageName = PACKAGE_NAME, - areRecommendationsValid = true, - mediaRecs = - listOf( - MediaRecModel(icon = icon), - MediaRecModel(icon = icon), - MediaRecModel(icon = icon), - ), - ) - - mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, smartspaceMediaData) - - assertThat(recommendations).isEqualTo(model) - } - - @Test - fun addInvalidRecommendation() = - testScope.runTest { - val recommendations by collectLastValue(underTest.recommendations) - val inValidData = smartspaceMediaData.copy(recommendations = listOf()) - - mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, smartspaceMediaData) - assertThat(recommendations?.areRecommendationsValid).isTrue() - - mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, inValidData) - assertThat(recommendations?.areRecommendationsValid).isFalse() - assertThat(recommendations?.mediaRecs?.isEmpty()).isTrue() - } - - @Test - fun removeRecommendation_noTrampolineActivity() { - val intent = Intent() - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, smartspaceMediaData) - underTest.removeMediaRecommendations(KEY_MEDIA_SMARTSPACE, intent, 0) - - verify(kosmos.mockBroadcastSender).sendBroadcast(eq(intent)) - } - - @Test - fun removeRecommendation_usingTrampolineActivity() { - doNothing().whenever(spyContext).startActivity(any()) - val intent = Intent() - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.component = ComponentName(PACKAGE_NAME, EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) - - underTest.removeMediaRecommendations(KEY_MEDIA_SMARTSPACE, intent, 0) - - verify(spyContext).startActivity(eq(intent)) - } - - @Test - fun startSettings() { - underTest.startSettings() - - verify(activityStarter).startActivity(any(), eq(true)) - } - - @Test - fun startClickIntent() { - doNothing().whenever(spyContext).startActivity(any()) - val intent = Intent() - val expandable = mock<Expandable>() - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, smartspaceMediaData) - underTest.startClickIntent(expandable, intent) - - verify(spyContext).startActivity(eq(intent)) - } - - companion object { - private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" - private const val PACKAGE_NAME = "com.example.app" - private const val SURFACE = 4 - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt index 005424ba599e..faa62c2febc1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/util/MediaDiffUtilTest.kt @@ -23,7 +23,6 @@ import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel import com.android.systemui.media.controls.ui.viewmodel.mediaControlViewModel -import com.android.systemui.media.controls.ui.viewmodel.mediaRecommendationsViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Assert.fail @@ -56,25 +55,6 @@ class MediaDiffUtilTest : SysuiTestCase() { } @Test - fun newMediaRecommendationsAdded() { - val mediaRecs = createMediaRecommendations(KEY_MEDIA_SMARTSPACE, true) - val oldList = listOf<MediaCommonViewModel>() - val newList = listOf(mediaRecs) - val mediaLoadedCallback = MediaViewModelCallback(oldList, newList) - val mediaLoadedListUpdateCallback = - MediaViewModelListUpdateCallback( - oldList, - newList, - { commonViewModel, _ -> assertThat(commonViewModel).isEqualTo(mediaRecs) }, - { commonViewModel, _ -> fail("Unexpected to update $commonViewModel") }, - { fail("Unexpected to remove $it") }, - { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") }, - ) - - DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback) - } - - @Test fun updateMediaControl_contentChanged() { val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123), true) val oldList = listOf(mediaControl) @@ -94,25 +74,6 @@ class MediaDiffUtilTest : SysuiTestCase() { } @Test - fun updateMediaRecommendations_contentChanged() { - val mediaRecs = createMediaRecommendations(KEY_MEDIA_SMARTSPACE, true) - val oldList = listOf(mediaRecs) - val newList = listOf(mediaRecs.copy(key = KEY_MEDIA_SMARTSPACE_2)) - val mediaLoadedCallback = MediaViewModelCallback(oldList, newList) - val mediaLoadedListUpdateCallback = - MediaViewModelListUpdateCallback( - oldList, - newList, - { commonViewModel, _ -> fail("Unexpected to add $commonViewModel") }, - { commonViewModel, _ -> assertThat(commonViewModel).isNotEqualTo(mediaRecs) }, - { fail("Unexpected to remove $it") }, - { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") }, - ) - - DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback) - } - - @Test fun mediaControlMoved() { val mediaControl1 = createMediaControl(InstanceId.fakeInstanceId(123), true) val mediaControl2 = createMediaControl(InstanceId.fakeInstanceId(456), false) @@ -133,27 +94,6 @@ class MediaDiffUtilTest : SysuiTestCase() { } @Test - fun mediaRecommendationsMoved() { - val mediaControl1 = createMediaControl(InstanceId.fakeInstanceId(123), true) - val mediaControl2 = createMediaControl(InstanceId.fakeInstanceId(456), false) - val mediaRecs = createMediaRecommendations(KEY_MEDIA_SMARTSPACE, true) - val oldList = listOf(mediaRecs, mediaControl1, mediaControl2) - val newList = listOf(mediaControl1, mediaControl2, mediaRecs) - val mediaLoadedCallback = MediaViewModelCallback(oldList, newList) - val mediaLoadedListUpdateCallback = - MediaViewModelListUpdateCallback( - oldList, - newList, - { commonViewModel, _ -> fail("Unexpected to add $commonViewModel") }, - { commonViewModel, _ -> fail("Unexpected to update $commonViewModel") }, - { fail("Unexpected to remove $it") }, - { commonViewModel, _, _ -> assertThat(commonViewModel).isEqualTo(mediaRecs) }, - ) - - DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback) - } - - @Test fun mediaControlRemoved() { val mediaControl = createMediaControl(InstanceId.fakeInstanceId(123), true) val oldList = listOf(mediaControl) @@ -172,25 +112,6 @@ class MediaDiffUtilTest : SysuiTestCase() { DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback) } - @Test - fun mediaRecommendationsRemoved() { - val mediaRecs = createMediaRecommendations(KEY_MEDIA_SMARTSPACE_2, false) - val oldList = listOf(mediaRecs) - val newList = listOf<MediaCommonViewModel>() - val mediaLoadedCallback = MediaViewModelCallback(oldList, newList) - val mediaLoadedListUpdateCallback = - MediaViewModelListUpdateCallback( - oldList, - newList, - { commonViewModel, _ -> fail("Unexpected to add $commonViewModel") }, - { commonViewModel, _ -> fail("Unexpected to update $commonViewModel") }, - { commonViewModel -> assertThat(commonViewModel).isEqualTo(mediaRecs) }, - { commonViewModel, _, _ -> fail("Unexpected to move $commonViewModel ") }, - ) - - DiffUtil.calculateDiff(mediaLoadedCallback).dispatchUpdatesTo(mediaLoadedListUpdateCallback) - } - private fun createMediaControl( instanceId: InstanceId, immediatelyUpdateUi: Boolean, @@ -201,26 +122,7 @@ class MediaDiffUtilTest : SysuiTestCase() { controlViewModel = kosmos.mediaControlViewModel, onAdded = {}, onRemoved = {}, - onUpdated = {} - ) - } - - private fun createMediaRecommendations( - key: String, - loadingEnabled: Boolean, - ): MediaCommonViewModel.MediaRecommendations { - return MediaCommonViewModel.MediaRecommendations( - key = key, - loadingEnabled = loadingEnabled, - recsViewModel = kosmos.mediaRecommendationsViewModel, - onAdded = {}, - onRemoved = {}, - onUpdated = {} + onUpdated = {}, ) } - - companion object { - private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" - private const val KEY_MEDIA_SMARTSPACE_2 = "MEDIA_SMARTSPACE_ID_2" - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt index fb5bbf452cfa..e56b114dc847 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt @@ -19,23 +19,18 @@ package com.android.systemui.media.controls.ui.viewmodel import android.R import android.content.packageManager import android.content.pm.ApplicationInfo -import android.graphics.drawable.Icon import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope -import com.android.systemui.media.controls.MediaTestHelper import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor -import com.android.systemui.media.controls.domain.pipeline.interactor.mediaRecommendationsInteractor import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter import com.android.systemui.media.controls.shared.mediaLogger import com.android.systemui.media.controls.shared.mockMediaLogger import com.android.systemui.media.controls.shared.model.MediaData -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData -import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -62,15 +57,7 @@ class MediaCarouselViewModelTest : SysuiTestCase() { private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager private val packageManager = kosmos.packageManager - private val icon = Icon.createWithResource(context, R.drawable.ic_media_play) private val drawable = context.getDrawable(R.drawable.ic_media_play) - private val smartspaceMediaData: SmartspaceMediaData = - SmartspaceMediaData( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - packageName = PACKAGE_NAME, - recommendations = MediaTestHelper.getValidRecommendationList(icon), - ) private val underTest: MediaCarouselViewModel = kosmos.mediaCarouselViewModel @@ -121,53 +108,6 @@ class MediaCarouselViewModelTest : SysuiTestCase() { } @Test - fun loadMediaControlsAndRecommendations_mediaItemsAreUpdated() = - testScope.runTest { - val sortedMedia by collectLastValue(underTest.mediaItems) - val instanceId1 = InstanceId.fakeInstanceId(123) - val instanceId2 = InstanceId.fakeInstanceId(456) - - loadMediaControl(KEY, instanceId1) - loadMediaControl(KEY_2, instanceId2) - loadMediaRecommendations() - - val firstMediaControl = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl - val secondMediaControl = sortedMedia?.get(1) as MediaCommonViewModel.MediaControl - val recsCard = sortedMedia?.get(2) as MediaCommonViewModel.MediaRecommendations - assertThat(firstMediaControl.instanceId).isEqualTo(instanceId2) - assertThat(secondMediaControl.instanceId).isEqualTo(instanceId1) - assertThat(recsCard.key).isEqualTo(KEY_MEDIA_SMARTSPACE) - } - - @Test - fun recommendationClicked_switchToPlayer() = - testScope.runTest { - val sortedMedia by collectLastValue(underTest.mediaItems) - kosmos.visualStabilityProvider.isReorderingAllowed = false - val instanceId = InstanceId.fakeInstanceId(123) - - loadMediaRecommendations() - kosmos.mediaRecommendationsInteractor.switchToMediaControl(PACKAGE_NAME) - - var recsCard = sortedMedia?.get(0) as MediaCommonViewModel.MediaRecommendations - assertThat(sortedMedia).hasSize(1) - assertThat(recsCard.key).isEqualTo(KEY_MEDIA_SMARTSPACE) - - loadMediaControl(KEY, instanceId, false) - - recsCard = sortedMedia?.get(0) as MediaCommonViewModel.MediaRecommendations - assertThat(sortedMedia).hasSize(1) - assertThat(recsCard.key).isEqualTo(KEY_MEDIA_SMARTSPACE) - - loadMediaControl(KEY, instanceId, true) - - val mediaControl = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl - assertThat(sortedMedia).hasSize(2) - assertThat(mediaControl.instanceId).isEqualTo(instanceId) - assertThat(mediaControl.isMediaFromRec).isTrue() - } - - @Test fun addMediaControlThenRemove_mediaEventsAreLogged() = testScope.runTest { val sortedMedia by collectLastValue(underTest.mediaItems) @@ -199,31 +139,6 @@ class MediaCarouselViewModelTest : SysuiTestCase() { verify(kosmos.mediaLogger).logMediaCardRemoved(eq(instanceId)) } - @Test - fun addMediaRecommendationThenRemove_mediaEventsAreLogged() = - testScope.runTest { - val sortedMedia by collectLastValue(underTest.mediaItems) - - loadMediaRecommendations() - - val mediaRecommendations = - sortedMedia?.get(0) as MediaCommonViewModel.MediaRecommendations - assertThat(mediaRecommendations.key).isEqualTo(KEY_MEDIA_SMARTSPACE) - - // when media recommendation is added to carousel - mediaRecommendations.onAdded(mediaRecommendations) - - verify(kosmos.mediaLogger).logMediaRecommendationCardAdded(eq(KEY_MEDIA_SMARTSPACE)) - - mediaDataFilter.onSmartspaceMediaDataRemoved(KEY, true) - assertThat(sortedMedia).isEmpty() - - // when media recommendation is removed from carousel - mediaRecommendations.onRemoved(true) - - verify(kosmos.mediaLogger).logMediaRecommendationCardRemoved(eq(KEY_MEDIA_SMARTSPACE)) - } - private fun loadMediaControl(key: String, instanceId: InstanceId, isPlaying: Boolean = true) { whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) @@ -239,15 +154,10 @@ class MediaCarouselViewModelTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(key, key, mediaData) } - private fun loadMediaRecommendations(key: String = KEY_MEDIA_SMARTSPACE) { - mediaDataFilter.onSmartspaceMediaDataLoaded(key, smartspaceMediaData) - } - companion object { private const val USER_ID = 0 private const val KEY = "key" private const val KEY_2 = "key2" private const val PACKAGE_NAME = "com.example.app" - private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelTest.kt deleted file mode 100644 index 51b1911be5d5..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.ui.viewmodel - -import android.R -import android.content.packageManager -import android.content.pm.ApplicationInfo -import android.graphics.drawable.Icon -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testScope -import com.android.systemui.media.controls.MediaTestHelper -import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl -import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData -import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers -import org.mockito.Mockito - -@SmallTest -@RunWith(AndroidJUnit4::class) -class MediaRecommendationsViewModelTest : SysuiTestCase() { - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - - private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter - private val packageManager = kosmos.packageManager - private val icon: Icon = Icon.createWithResource(context, R.drawable.ic_media_play) - private val drawable = context.getDrawable(R.drawable.ic_media_play) - private val smartspaceMediaData: SmartspaceMediaData = - SmartspaceMediaData( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - packageName = PACKAGE_NAME, - recommendations = MediaTestHelper.getValidRecommendationList(icon), - ) - - private val underTest: MediaRecommendationsViewModel = kosmos.mediaRecommendationsViewModel - - @Test - fun loadRecommendations_recsCardViewModelIsLoaded() = - testScope.runTest { - whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable) - whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java))) - .thenReturn(drawable) - whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt())) - .thenReturn(ApplicationInfo()) - whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME) - val recsCardViewModel by collectLastValue(underTest.mediaRecsCard) - - context.setMockPackageManager(packageManager) - - mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, smartspaceMediaData) - - assertThat(recsCardViewModel).isNotNull() - assertThat(recsCardViewModel?.mediaRecs?.size) - .isEqualTo(smartspaceMediaData.recommendations.size) - } - - companion object { - private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" - private const val PACKAGE_NAME = "com.example.app" - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateOverrideTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateOverrideTest.kt new file mode 100644 index 000000000000..24bd8adaa45a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUIStateOverrideTest.kt @@ -0,0 +1,105 @@ +/* + * 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.model + +import android.view.Display +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.dumpManager +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.never +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SysUIStateOverrideTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private val defaultState = kosmos.sysUiState + private val callbackOnOverride = mock<SysUiState.SysUiStateCallback>() + private val dumpManager = kosmos.dumpManager + + private val underTest = kosmos.sysUiStateOverrideFactory.invoke(DISPLAY_1) + + @Before + fun setup() { + underTest.start() + underTest.addCallback(callbackOnOverride) + reset(callbackOnOverride) + } + + @Test + fun setFlag_setOnDefaultState_propagatedToOverride() { + defaultState.setFlag(FLAG_1, true).commitUpdate() + + verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY) + verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, DISPLAY_1) + } + + @Test + fun setFlag_onOverride_overridesDefaultOnes() { + defaultState.setFlag(FLAG_1, false).setFlag(FLAG_2, true).commitUpdate() + underTest.setFlag(FLAG_1, true).setFlag(FLAG_2, false).commitUpdate() + + assertThat(underTest.isFlagEnabled(FLAG_1)).isTrue() + assertThat(underTest.isFlagEnabled(FLAG_2)).isFalse() + + assertThat(defaultState.isFlagEnabled(FLAG_1)).isFalse() + assertThat(defaultState.isFlagEnabled(FLAG_2)).isTrue() + } + + @Test + fun destroy_callbacksForDefaultStateNotReceivedAnymore() { + defaultState.setFlag(FLAG_1, true).commitUpdate() + + verify(callbackOnOverride).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY) + + reset(callbackOnOverride) + underTest.destroy() + defaultState.setFlag(FLAG_1, false).commitUpdate() + + verify(callbackOnOverride, never()).onSystemUiStateChanged(FLAG_1, Display.DEFAULT_DISPLAY) + } + + @Test + fun init_registersWithDumpManager() { + verify(dumpManager).registerNormalDumpable(any(), eq(underTest)) + } + + @Test + fun destroy_unregistersWithDumpManager() { + underTest.destroy() + + verify(dumpManager).unregisterDumpable(ArgumentMatchers.anyString()) + } + + private companion object { + const val DISPLAY_1 = 1 + const val FLAG_1 = 1L + const val FLAG_2 = 2L + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java index f6de6295212b..5bb7f36e4187 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/model/SysUiStateTest.java @@ -140,6 +140,8 @@ public class SysUiStateTest extends SysuiTestCase { @Test public void init_registersWithDumpManager() { + mFlagsContainer.start(); + verify(mDumpManager).registerNormalDumpable(any(), eq(mFlagsContainer)); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt index 0448ad517c6d..0a82f72f8f21 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt @@ -30,6 +30,9 @@ import com.android.systemui.shared.Flags import com.google.common.truth.Truth.assertThat import java.util.UUID import kotlin.test.Test +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.runner.RunWith import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.kotlin.any @@ -43,10 +46,19 @@ import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) class DefaultScreenshotActionsProviderTest : SysuiTestCase() { + private val scheduler = TestCoroutineScheduler() + private val mainDispatcher = UnconfinedTestDispatcher(scheduler) + private val testScope = TestScope(mainDispatcher) private val actionExecutor = mock<ActionExecutor>() private val uiEventLogger = mock<UiEventLogger>() private val actionsCallback = mock<ScreenshotActionsController.ActionsCallback>() - private val actionIntentCreator = ActionIntentCreator(context, context.packageManager) + private val actionIntentCreator = + ActionIntentCreator( + context, + context.packageManager, + testScope.backgroundScope, + mainDispatcher, + ) private val request = ScreenshotData.forTesting(userHandle = UserHandle.OWNER) private val validResult = ScreenshotSavedResult(Uri.EMPTY, Process.myUserHandle(), 0) @@ -198,6 +210,7 @@ class DefaultScreenshotActionsProviderTest : SysuiTestCase() { context, uiEventLogger, actionIntentCreator, + testScope, UUID.randomUUID(), request, actionExecutor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 2c852c3f6185..94db429c2225 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -598,7 +598,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mKeyguardClockPositionAlgorithm, mMSDLPlayer, mBrightnessMirrorShowingRepository, - new BlurConfig(0f, 0f)); + new BlurConfig(0f, 0f), + () -> mKosmos.getFakeShadeDisplaysRepository()); mNotificationPanelViewController.initDependencies( mCentralSurfaces, null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index f54c36754d31..89263fc42739 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -195,9 +195,7 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) public void updateSystemUiStateFlags_updatesSysuiStateInteractor() { var DISPLAY_ID = 10; - var displayMock = display(TYPE_INTERNAL, /* flags= */ 0, /* id= */DISPLAY_ID, - /* state= */ null); - when(mView.getDisplay()).thenReturn(displayMock); + mKosmos.getFakeShadeDisplaysRepository().setPendingDisplayId(DISPLAY_ID); mNotificationPanelViewController.updateSystemUiStateFlags(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt index 9498daaf0b07..c635c7ff69a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt @@ -67,7 +67,7 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() { fun policy_changing_propagatedFromTheLatestPolicy() = testScope.runTest { val underTest = createUnderTest() - val displayIds by collectValues(underTest.displayId) + val displayIds by collectValues(underTest.pendingDisplayId) assertThat(displayIds).containsExactly(0) @@ -98,7 +98,7 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() { DEVELOPMENT_SHADE_DISPLAY_AWARENESS, FakeShadeDisplayPolicy.name, ) - val displayId by collectLastValue(underTest.displayId) + val displayId by collectLastValue(underTest.pendingDisplayId) displayRepository.addDisplay(displayId = 1) @@ -161,7 +161,7 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() { FakeShadeDisplayPolicy.name, ) - val displayId by collectLastValue(underTest.displayId) + val displayId by collectLastValue(underTest.pendingDisplayId) displayRepository.addDisplays(display(id = 2, type = TYPE_EXTERNAL)) FakeShadeDisplayPolicy.setDisplayId(2) @@ -176,4 +176,17 @@ class ShadeDisplaysRepositoryTest : SysuiTestCase() { assertThat(displayId).isEqualTo(2) } + + @Test + fun onDisplayChangedSucceeded_displayIdChanges() = + testScope.runTest { + val underTest = createUnderTest() + val displayId by collectLastValue(underTest.displayId) + + assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) + + underTest.onDisplayChangedSucceeded(1) + + assertThat(displayId).isEqualTo(1) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt index fd6bc98b006c..f51d41bbc80b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt @@ -69,7 +69,7 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() { @Test fun commandShadeDisplayOverride_resetsDisplayId() = testScope.runTest { - val displayId by collectLastValue(shadeDisplaysRepository.displayId) + val displayId by collectLastValue(shadeDisplaysRepository.pendingDisplayId) assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) val newDisplayId = 2 @@ -87,7 +87,7 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() { @Test fun commandShadeDisplayOverride_anyExternalDisplay_notOnDefaultAnymore() = testScope.runTest { - val displayId by collectLastValue(shadeDisplaysRepository.displayId) + val displayId by collectLastValue(shadeDisplaysRepository.pendingDisplayId) assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) val newDisplayId = 2 displayRepository.addDisplay(displayId = newDisplayId) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt index 0ad60b617194..03f546b09faf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt @@ -22,7 +22,6 @@ import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.ui.data.repository.configurationRepository import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher @@ -82,7 +81,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { fun start_shadeInCorrectPosition_notAddedOrRemoved() = testScope.runTest { whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(0) + positionRepository.setPendingDisplayId(0) underTest.start() @@ -93,18 +92,19 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { fun start_shadeInWrongPosition_changes() = testScope.runTest { whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + positionRepository.setPendingDisplayId(1) underTest.start() verify(shadeContext).reparentToDisplay(eq(1)) + assertThat(positionRepository.displayId.value).isEqualTo(1) } @Test fun start_shadeInWrongPosition_logsStartToLatencyTracker() = testScope.runTest { whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + positionRepository.setPendingDisplayId(1) underTest.start() @@ -115,7 +115,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { fun start_shadeInWrongPosition_someNotificationsVisible_hiddenThenShown() = testScope.runTest { whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + positionRepository.setPendingDisplayId(1) activeNotificationRepository.setActiveNotifs(1) underTest.start() @@ -129,7 +129,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { fun start_shadeInWrongPosition_someNotificationsVisible_waitsForInflationsBeforeShowingNssl() = testScope.runTest { whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + positionRepository.setPendingDisplayId(1) activeNotificationRepository.setActiveNotifs(1) val endRebinding = notificationRebindingTracker.trackRebinding("test") @@ -155,7 +155,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { fun start_shadeInWrongPosition_noNotifications_nsslNotHidden() = testScope.runTest { whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + positionRepository.setPendingDisplayId(1) activeNotificationRepository.setActiveNotifs(0) underTest.start() @@ -170,7 +170,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { fun start_shadeInWrongPosition_waitsUntilMovedToDisplayReceived() = testScope.runTest { whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + positionRepository.setPendingDisplayId(1) activeNotificationRepository.setActiveNotifs(1) underTest.start() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index 4f301031e77c..fad66581682f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -108,7 +108,7 @@ class DefaultClockProviderTest : SysuiTestCase() { verify(mockSmallClockView).setColors(DOZE_COLOR, Color.MAGENTA) verify(mockLargeClockView).setColors(DOZE_COLOR, Color.MAGENTA) - clock.initialize(true, 0f, 0f, {}) + clock.initialize(true, 0f, 0f, null) val expectedColor = 0 verify(mockSmallClockView).setColors(DOZE_COLOR, expectedColor) 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 326d8ffd3c7c..8165d45c93df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shared.Flags as SharedFlags import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScrimController @@ -43,10 +44,11 @@ import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.testKosmos import com.android.systemui.util.WallpaperController import com.android.systemui.util.mockito.eq -import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor 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 import org.junit.Test @@ -67,8 +69,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 -import java.util.function.Consumer @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -76,7 +76,6 @@ import java.util.function.Consumer class NotificationShadeDepthControllerTest : SysuiTestCase() { private val kosmos = testKosmos() - private val applicationScope = kosmos.testScope.backgroundScope @Mock private lateinit var windowRootViewBlurInteractor: WindowRootViewBlurInteractor @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var blurUtils: BlurUtils @@ -85,7 +84,6 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Mock private lateinit var keyguardInteractor: KeyguardInteractor @Mock private lateinit var choreographer: Choreographer @Mock private lateinit var wallpaperController: WallpaperController - @Mock private lateinit var wallpaperInteractor: WallpaperInteractor @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var appZoomOutOptional: Optional<AppZoomOut> @@ -130,14 +128,12 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { keyguardInteractor, choreographer, wallpaperController, - wallpaperInteractor, notificationShadeWindowController, dozeParameters, context, ResourcesSplitShadeStateController(), windowRootViewBlurInteractor, appZoomOutOptional, - applicationScope, dumpManager, configurationController, ) @@ -314,21 +310,19 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test - fun onDozeAmountChanged_doesNotApplyBlurWithAmbientAod() { - notificationShadeDepthController.wallpaperSupportsAmbientMode = false - + @DisableFlags(SharedFlags.FLAG_AMBIENT_AOD) + fun onDozeAmountChanged_appliesBlur() { statusBarStateListener.onDozeAmountChanged(1f, 1f) notificationShadeDepthController.updateBlurCallback.doFrame(0) - verify(blurUtils).applyBlur(any(), eq(0), eq(false)) + verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false)) } @Test - fun onDozeAmountChanged_appliesBlurWithAmbientAod() { - notificationShadeDepthController.wallpaperSupportsAmbientMode = true - + @EnableFlags(SharedFlags.FLAG_AMBIENT_AOD) + fun onDozeAmountChanged_doesNotApplyBlurWithAmbientAod() { statusBarStateListener.onDozeAmountChanged(1f, 1f) notificationShadeDepthController.updateBlurCallback.doFrame(0) - verify(blurUtils).applyBlur(any(), eq(maxBlur), eq(false)) + verify(blurUtils).applyBlur(any(), eq(0), eq(false)) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt index 7ed2bd38bcd2..0b9b297130a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt @@ -34,6 +34,8 @@ import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.addNotif +import com.android.systemui.statusbar.notification.data.repository.removeNotif import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType @@ -409,6 +411,63 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun shownNotificationChips_lastAppVisibleTimeMaintainedAcrossNotifAddsAndRemoves() = + kosmos.runTest { + val latest by collectLastValue(underTest.shownNotificationChips) + + val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) + val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) + + // Have notif1's app start as showing and then hide later so we get the chip + activityManagerRepository.fake.startingIsAppVisibleValue = true + fakeSystemClock.setCurrentTimeMillis(9_000) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = notif1Info.key, + uid = notif1Info.uid, + statusBarChipIcon = notif1Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif1Info.key).build(), + ) + ) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false) + + assertThat(latest!![0].key).isEqualTo(notif1Info.key) + assertThat(latest!![0].lastAppVisibleTime).isEqualTo(9_000) + + // WHEN a new notification is added + activityManagerRepository.fake.startingIsAppVisibleValue = true + fakeSystemClock.setCurrentTimeMillis(10_000) + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif2Info.key).build(), + ) + ) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN the new notification is first + assertThat(latest!![0].key).isEqualTo(notif2Info.key) + assertThat(latest!![0].lastAppVisibleTime).isEqualTo(10_000) + + // And THEN the original notification maintains its lastAppVisibleTime + assertThat(latest!![1].key).isEqualTo(notif1Info.key) + assertThat(latest!![1].lastAppVisibleTime).isEqualTo(9_000) + + // WHEN notif1 is removed + fakeSystemClock.setCurrentTimeMillis(11_000) + activeNotificationListRepository.removeNotif(notif1Info.key) + + // THEN notif2 still has its lastAppVisibleTime + assertThat(latest!![0].key).isEqualTo(notif2Info.key) + assertThat(latest!![0].lastAppVisibleTime).isEqualTo(10_000) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun shownNotificationChips_sortedByLastAppVisibleTime() = kosmos.runTest { val latest by collectLastValue(underTest.shownNotificationChips) 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 1b5b0d6ff897..e2d1498270c8 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 @@ -16,18 +16,19 @@ package com.android.systemui.statusbar.chips.ui.viewmodel +import android.content.Context import android.content.DialogInterface import android.content.packageManager import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.view.View 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.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue @@ -181,7 +182,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.primaryChip) - assertIsCallChip(latest, notificationKey) + assertIsCallChip(latest, notificationKey, context) } @Test @@ -196,7 +197,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.primaryChip) - assertIsCallChip(latest, callNotificationKey) + assertIsCallChip(latest, callNotificationKey, context) // WHEN the higher priority media projection chip is added mediaProjectionState.value = @@ -241,7 +242,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.NotProjecting // THEN the lower priority call is used - assertIsCallChip(latest, callNotificationKey) + assertIsCallChip(latest, callNotificationKey, context) } /** Regression test for b/347726238. */ @@ -395,18 +396,29 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all) } - fun assertIsCallChip(latest: OngoingActivityChipModel?, notificationKey: String) { + fun assertIsCallChip( + latest: OngoingActivityChipModel?, + notificationKey: String, + context: Context, + ) { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active::class.java) assertThat((latest as OngoingActivityChipModel.Active).key).isEqualTo(notificationKey) if (StatusBarConnectedDisplays.isEnabled) { assertNotificationIcon(latest, notificationKey) - return + } else { + val contentDescription = + if (latest.icon is OngoingActivityChipModel.ChipIcon.SingleColorIcon) { + ((latest.icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon) + .impl + .contentDescription + } else { + (latest.icon as OngoingActivityChipModel.ChipIcon.StatusBarView) + .contentDescription + } + assertThat(contentDescription.loadContentDescription(context)) + .contains(context.getString(R.string.ongoing_call_content_description)) } - val icon = - ((latest.icon) as OngoingActivityChipModel.ChipIcon.SingleColorIcon).impl - as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) } private fun assertNotificationIcon( 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 2887de38fe23..96c4a59f752d 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 @@ -48,6 +48,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip +import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModelLegacy import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel @@ -172,6 +173,18 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { } @Test + fun visibleChipKeys_allInactive() = + kosmos.runTest { + val latest by collectLastValue(underTest.visibleChipKeys) + + screenRecordState.value = ScreenRecordModel.DoingNothing + mediaProjectionState.value = MediaProjectionState.NotProjecting + setNotifs(emptyList()) + + assertThat(latest).isEmpty() + } + + @Test fun primaryChip_screenRecordShow_restHidden_screenRecordShown() = kosmos.runTest { screenRecordState.value = ScreenRecordModel.Recording @@ -241,10 +254,24 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chips) assertIsScreenRecordChip(latest!!.primary) - assertIsCallChip(latest!!.secondary, callNotificationKey) + assertIsCallChip(latest!!.secondary, callNotificationKey, context) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } + @Test + fun visibleChipKeys_screenRecordShowAndCallShow_hasBothKeys() = + kosmos.runTest { + val latest by collectLastValue(underTest.visibleChipKeys) + + val callNotificationKey = "call" + screenRecordState.value = ScreenRecordModel.Recording + addOngoingCallState(callNotificationKey) + + assertThat(latest) + .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey) + .inOrder() + } + @EnableChipsModernization @Test fun chips_screenRecordAndCallActive_inThatOrder() = @@ -258,7 +285,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(latest!!.active.size).isEqualTo(2) assertIsScreenRecordChip(latest!!.active[0]) - assertIsCallChip(latest!!.active[1], callNotificationKey) + assertIsCallChip(latest!!.active[1], callNotificationKey, context) assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(2) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) @@ -590,7 +617,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chips) assertIsShareToAppChip(latest!!.primary) - assertIsCallChip(latest!!.secondary, callNotificationKey) + assertIsCallChip(latest!!.secondary, callNotificationKey, context) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } @@ -609,7 +636,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(latest!!.active.size).isEqualTo(2) assertIsShareToAppChip(latest!!.active[0]) - assertIsCallChip(latest!!.active[1], callNotificationKey) + assertIsCallChip(latest!!.active[1], callNotificationKey, context) assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(2) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) @@ -627,7 +654,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.primaryChip) - assertIsCallChip(latest, callNotificationKey) + assertIsCallChip(latest, callNotificationKey, context) } @DisableChipsModernization @@ -644,7 +671,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val latest by collectLastValue(underTest.chipsLegacy) val unused by collectLastValue(underTest.chips) - assertIsCallChip(latest!!.primary, callNotificationKey) + assertIsCallChip(latest!!.primary, callNotificationKey, context) assertThat(latest!!.secondary) .isInstanceOf(OngoingActivityChipModel.Inactive::class.java) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) @@ -664,7 +691,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { val unused by collectLastValue(underTest.chipsLegacy) assertThat(latest!!.active.size).isEqualTo(1) - assertIsCallChip(latest!!.active[0], callNotificationKey) + assertIsCallChip(latest!!.active[0], callNotificationKey, context) assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(3) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) @@ -864,6 +891,37 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy()) } + @Test + fun visibleChipKeys_threePromotedNotifs_topTwoInList() = + kosmos.runTest { + val latest by collectLastValue(underTest.visibleChipKeys) + + setNotifs( + listOf( + activeNotificationModel( + key = "firstNotif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = + PromotedNotificationContentModel.Builder("firstNotif").build(), + ), + activeNotificationModel( + key = "secondNotif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = + PromotedNotificationContentModel.Builder("secondNotif").build(), + ), + activeNotificationModel( + key = "thirdNotif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = + PromotedNotificationContentModel.Builder("thirdNotif").build(), + ), + ) + ) + + assertThat(latest).containsExactly("firstNotif", "secondNotif").inOrder() + } + @DisableChipsModernization @Test fun chipsLegacy_callAndPromotedNotifs_primaryIsCallSecondaryIsNotif() = @@ -892,7 +950,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) ) - assertIsCallChip(latest!!.primary, callNotificationKey) + assertIsCallChip(latest!!.primary, callNotificationKey, context) assertIsNotifChip(latest!!.secondary, context, firstIcon, "firstNotif") assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } @@ -926,7 +984,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) assertThat(latest!!.active.size).isEqualTo(2) - assertIsCallChip(latest!!.active[0], callNotificationKey) + assertIsCallChip(latest!!.active[0], callNotificationKey, context) assertIsNotifChip(latest!!.active[1], context, firstIcon, "firstNotif") assertThat(latest!!.overflow.size).isEqualTo(1) assertIsNotifChip(latest!!.overflow[0], context, secondIcon, "secondNotif") @@ -953,10 +1011,31 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { ) assertIsScreenRecordChip(latest!!.primary) - assertIsCallChip(latest!!.secondary, callNotificationKey) + assertIsCallChip(latest!!.secondary, callNotificationKey, context) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) } + @Test + fun visibleChipKeys_screenRecordAndCallAndPromotedNotifs_topTwoInList() = + kosmos.runTest { + val latest by collectLastValue(underTest.visibleChipKeys) + + val callNotificationKey = "call" + addOngoingCallState(callNotificationKey) + screenRecordState.value = ScreenRecordModel.Recording + activeNotificationListRepository.addNotif( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = PromotedNotificationContentModel.Builder("notif").build(), + ) + ) + + assertThat(latest) + .containsExactly(ScreenRecordChipViewModel.KEY, callNotificationKey) + .inOrder() + } + @EnableChipsModernization @Test fun chips_screenRecordAndCallAndPromotedNotif_notifInOverflow() = @@ -978,7 +1057,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { assertThat(latest!!.active.size).isEqualTo(2) assertIsScreenRecordChip(latest!!.active[0]) - assertIsCallChip(latest!!.active[1], callNotificationKey) + assertIsCallChip(latest!!.active[1], callNotificationKey, context) assertThat(latest!!.overflow.size).isEqualTo(1) assertIsNotifChip(latest!!.overflow[0], context, notifIcon, "notif") assertThat(latest!!.inactive.size).isEqualTo(2) @@ -1013,7 +1092,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { addOngoingCallState(callNotificationKey) // THEN the higher priority call chip is used - assertIsCallChip(latest, callNotificationKey) + assertIsCallChip(latest, callNotificationKey, context) // WHEN the higher priority media projection chip is added mediaProjectionState.value = @@ -1066,7 +1145,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { mediaProjectionState.value = MediaProjectionState.NotProjecting // THEN the lower priority call is used - assertIsCallChip(latest, callNotificationKey) + assertIsCallChip(latest, callNotificationKey, context) // WHEN the higher priority call is removed removeOngoingCallState(key = callNotificationKey) @@ -1107,7 +1186,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // THEN the higher priority call chip is used as primary and notif is demoted to // secondary - assertIsCallChip(latest!!.primary, callNotificationKey) + assertIsCallChip(latest!!.primary, callNotificationKey, context) assertIsNotifChip(latest!!.secondary, context, notifIcon, "notif") assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) @@ -1122,7 +1201,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // THEN the higher priority media projection chip is used as primary and call is demoted // to secondary (and notif is dropped altogether) assertIsShareToAppChip(latest!!.primary) - assertIsCallChip(latest!!.secondary, callNotificationKey) + assertIsCallChip(latest!!.secondary, callNotificationKey, context) assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel()) // WHEN the higher priority screen record chip is added @@ -1185,7 +1264,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // THEN the higher priority call chip and notif are active in that order assertThat(latest!!.active.size).isEqualTo(2) - assertIsCallChip(latest!!.active[0], callNotificationKey) + assertIsCallChip(latest!!.active[0], callNotificationKey, context) assertIsNotifChip(latest!!.active[1], context, notifIcon, "notif") assertThat(latest!!.overflow).isEmpty() assertThat(latest!!.inactive.size).isEqualTo(3) @@ -1203,7 +1282,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // notif is demoted to overflow assertThat(latest!!.active.size).isEqualTo(2) assertIsShareToAppChip(latest!!.active[0]) - assertIsCallChip(latest!!.active[1], callNotificationKey) + assertIsCallChip(latest!!.active[1], callNotificationKey, context) assertThat(latest!!.overflow.size).isEqualTo(1) assertIsNotifChip(latest!!.overflow[0], context, notifIcon, "notif") assertThat(latest!!.inactive.size).isEqualTo(2) @@ -1216,7 +1295,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { // media projection and notif are demoted in overflow assertThat(latest!!.active.size).isEqualTo(2) assertIsScreenRecordChip(latest!!.active[0]) - assertIsCallChip(latest!!.active[1], callNotificationKey) + assertIsCallChip(latest!!.active[1], callNotificationKey, context) assertThat(latest!!.overflow.size).isEqualTo(2) assertIsShareToAppChip(latest!!.overflow[0]) assertIsNotifChip(latest!!.overflow[1], context, notifIcon, "notif") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt index d36dbbe8d36f..d4518e7299da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt @@ -21,9 +21,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.flags.parameterizeSceneContainerFlag 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.lifecycle.activateIn import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData @@ -47,7 +48,8 @@ import platform.test.runner.parameterized.Parameters class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val mediaControlChipInteractor by lazy { kosmos.mediaControlChipInteractor } - private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipViewModel } + private val Kosmos.underTest by + Kosmos.Fixture { kosmos.mediaControlChipViewModelFactory.create() } @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> companion object { @@ -62,6 +64,7 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas fun setUp() { MockitoAnnotations.initMocks(this) mediaControlChipInteractor.initialize() + kosmos.underTest.activateIn(kosmos.testScope) } init { @@ -71,7 +74,7 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun chip_noActiveMedia_IsHidden() = kosmos.runTest { - val chip by collectLastValue(underTest.chip) + val chip = underTest.chip assertThat(chip).isInstanceOf(PopupChipModel.Hidden::class.java) } @@ -79,30 +82,26 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun chip_activeMedia_IsShown() = kosmos.runTest { - val chip by collectLastValue(underTest.chip) - val userMedia = MediaData(active = true, song = "test") updateMedia(userMedia) - assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) + assertThat(underTest.chip).isInstanceOf(PopupChipModel.Shown::class.java) } @Test fun chip_songNameChanges_chipTextUpdated() = kosmos.runTest { - val chip by collectLastValue(underTest.chip) - val initialSongName = "Initial Song" val newSongName = "New Song" val userMedia = MediaData(active = true, song = initialSongName) updateMedia(userMedia) - assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) - assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) + assertThat(underTest.chip).isInstanceOf(PopupChipModel.Shown::class.java) + assertThat((underTest.chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) updateMedia(updatedUserMedia) - assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) + assertThat((underTest.chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) } private fun updateMedia(mediaData: MediaData) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt new file mode 100644 index 000000000000..134ab9322df0 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.featurepods.media.ui.viewmodel + +import android.platform.test.annotations.EnableFlags +import androidx.compose.runtime.snapshots.Snapshot +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId +import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModelFactory +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@EnableFlags(StatusBarPopupChips.FLAG_NAME) +@RunWith(AndroidJUnit4::class) +class StatusBarPopupChipsViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val underTest = kosmos.statusBarPopupChipsViewModelFactory.create() + + @Before + fun setUp() { + underTest.activateIn(kosmos.testScope) + } + + @Test + fun shownPopupChips_allHidden_empty() = + kosmos.runTest { + val shownPopupChips = underTest.shownPopupChips + assertThat(shownPopupChips).isEmpty() + } + + @Test + fun shownPopupChips_activeMedia_restHidden_mediaControlChipShown() = + kosmos.runTest { + val shownPopupChips = underTest.shownPopupChips + val userMedia = MediaData(active = true, song = "test") + val instanceId = userMedia.instanceId + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + Snapshot.takeSnapshot { + assertThat(shownPopupChips).hasSize(1) + assertThat(shownPopupChips.first().chipId).isEqualTo(PopupChipId.MediaControl) + } + } + + @Test + fun shownPopupChips_mediaChipToggled_popupShown() = + kosmos.runTest { + val shownPopupChips = underTest.shownPopupChips + + val userMedia = MediaData(active = true, song = "test") + val instanceId = userMedia.instanceId + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + Snapshot.takeSnapshot { + assertThat(shownPopupChips).hasSize(1) + val mediaChip = shownPopupChips.first() + assertThat(mediaChip.isPopupShown).isFalse() + + mediaChip.showPopup.invoke() + assertThat(shownPopupChips.first().isPopupShown).isTrue() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt index 0f1cdac05f7a..fdc6657acd1d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt @@ -31,6 +31,8 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.shade.domain.interactor.enableDualShade +import com.android.systemui.shade.domain.interactor.enableSingleShade import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN @@ -69,6 +71,7 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { @Test fun updateBounds() = testScope.runTest { + kosmos.enableSingleShade() val radius = MutableStateFlow(32) val leftOffset = MutableStateFlow(0) val shape by @@ -106,7 +109,7 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { ) ) - // When: QuickSettings shows up full screen + // When: QuickSettings shows up full screen on single shade. fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) val transitionState = MutableStateFlow<ObservableTransitionState>( @@ -115,6 +118,10 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { sceneInteractor.setTransitionState(transitionState) // Then: shape is null assertThat(shape).isNull() + + // Same scenario on Dual Shade, shape should have clipping bounds + kosmos.enableDualShade() + assertThat(shape).isNotNull() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapterTest.kt index 83fd5dcd5f82..a13b8647e170 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapterTest.kt @@ -34,123 +34,122 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper -class BundleEntryTest : SysuiTestCase() { - private lateinit var underTest: BundleEntry +class BundleEntryAdapterTest : SysuiTestCase() { + private lateinit var underTest: BundleEntryAdapter - @get:Rule - val setFlagsRule = SetFlagsRule() + @get:Rule val setFlagsRule = SetFlagsRule() @Before fun setUp() { - underTest = BundleEntry("key") + underTest = BundleEntryAdapter(BundleEntry("key")) } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getParent_adapter() { - assertThat(underTest.entryAdapter.parent).isEqualTo(GroupEntry.ROOT_ENTRY) + assertThat(underTest.parent).isEqualTo(GroupEntry.ROOT_ENTRY) } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isTopLevelEntry_adapter() { - assertThat(underTest.entryAdapter.isTopLevelEntry).isTrue() + assertThat(underTest.isTopLevelEntry).isTrue() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getRow_adapter() { - assertThat(underTest.entryAdapter.row).isNull() + assertThat(underTest.row).isNull() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupRoot_adapter() { - assertThat(underTest.entryAdapter.isGroupRoot).isTrue() + assertThat(underTest.isGroupRoot).isTrue() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getKey_adapter() { - assertThat(underTest.entryAdapter.key).isEqualTo("key") + assertThat(underTest.key).isEqualTo("key") } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isClearable_adapter() { - assertThat(underTest.entryAdapter.isClearable).isTrue() + assertThat(underTest.isClearable).isTrue() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getSummarization_adapter() { - assertThat(underTest.entryAdapter.summarization).isNull() + assertThat(underTest.summarization).isNull() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getContrastedColor_adapter() { - assertThat(underTest.entryAdapter.getContrastedColor(context, false, Color.WHITE)) + assertThat(underTest.getContrastedColor(context, false, Color.WHITE)) .isEqualTo(Notification.COLOR_DEFAULT) } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun canPeek_adapter() { - assertThat(underTest.entryAdapter.canPeek()).isFalse() + assertThat(underTest.canPeek()).isFalse() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getWhen_adapter() { - assertThat(underTest.entryAdapter.`when`).isEqualTo(0) + assertThat(underTest.`when`).isEqualTo(0) } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isColorized() { - assertThat(underTest.entryAdapter.isColorized).isFalse() + assertThat(underTest.isColorized).isFalse() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getSbn() { - assertThat(underTest.entryAdapter.sbn).isNull() + assertThat(underTest.sbn).isNull() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun canDragAndDrop() { - assertThat(underTest.entryAdapter.canDragAndDrop()).isFalse() + assertThat(underTest.canDragAndDrop()).isFalse() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isBubble() { - assertThat(underTest.entryAdapter.isBubbleCapable).isFalse() + assertThat(underTest.isBubbleCapable).isFalse() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getStyle() { - assertThat(underTest.entryAdapter.style).isNull() + assertThat(underTest.style).isNull() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun getSectionBucket() { - assertThat(underTest.entryAdapter.sectionBucket).isEqualTo(underTest.bucket) + assertThat(underTest.sectionBucket).isEqualTo(underTest.entry.bucket) } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isAmbient() { - assertThat(underTest.entryAdapter.isAmbient).isFalse() + assertThat(underTest.isAmbient).isFalse() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun canShowFullScreen() { - assertThat(underTest.entryAdapter.isFullScreenCapable()).isFalse() + assertThat(underTest.isFullScreenCapable()).isFalse() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt new file mode 100644 index 000000000000..b6889afa4e8a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt @@ -0,0 +1,370 @@ +/* + * 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.notification.collection + +import android.app.ActivityManager +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.os.UserHandle +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.entryAdapterFactory +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper +class NotificationEntryAdapterTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private val factory: EntryAdapterFactory = kosmos.entryAdapterFactory + private lateinit var underTest: NotificationEntryAdapter + + @get:Rule val setFlagsRule = SetFlagsRule() + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getParent_adapter() { + val ge = GroupEntryBuilder().build() + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .setParent(ge) + .build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.parent).isEqualTo(entry.parent) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isTopLevelEntry_adapter() { + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .setParent(GroupEntry.ROOT_ENTRY) + .build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.isTopLevelEntry).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getKey_adapter() { + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.key).isEqualTo(entry.key) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getRow_adapter() { + val row = Mockito.mock(ExpandableNotificationRow::class.java) + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .build() + entry.row = row + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.row).isEqualTo(entry.row) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_adapter_groupSummary() { + val row = Mockito.mock(ExpandableNotificationRow::class.java) + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setGroupSummary(true) + .setGroup("key") + .build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .setParent(GroupEntry.ROOT_ENTRY) + .build() + entry.row = row + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.isGroupRoot).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_adapter_groupChild() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setGroupSummary(true) + .setGroup("key") + .build() + + val parent = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + val groupEntry = GroupEntryBuilder().setSummary(parent) + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .setParent(groupEntry.build()) + .build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.isGroupRoot).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isClearable_adapter() { + val row = Mockito.mock(ExpandableNotificationRow::class.java) + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .build() + entry.row = row + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.isClearable).isEqualTo(entry.isClearable) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getSummarization_adapter() { + val row = Mockito.mock(ExpandableNotificationRow::class.java) + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .build() + val ranking = RankingBuilder(entry.ranking).setSummarization("hello").build() + entry.setRanking(ranking) + entry.row = row + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.summarization).isEqualTo("hello") + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getIcons_adapter() { + val row = Mockito.mock(ExpandableNotificationRow::class.java) + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setUser(UserHandle(ActivityManager.getCurrentUser())) + .build() + entry.row = row + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.icons).isEqualTo(entry.icons) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isColorized() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setColorized(true) + .build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.isColorized).isEqualTo(entry.sbn.notification.isColorized) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getSbn() { + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.sbn).isEqualTo(entry.sbn) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun canDragAndDrop() { + val pi = Mockito.mock(PendingIntent::class.java) + Mockito.`when`(pi.isActivity).thenReturn(true) + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setContentIntent(pi) + .build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.canDragAndDrop()).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isBubble() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setFlag(Notification.FLAG_BUBBLE, true) + .build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.isBubbleCapable).isEqualTo(entry.isBubble) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getStyle() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setStyle(Notification.BigTextStyle()) + .build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.style).isEqualTo(entry.notificationStyle) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getSectionBucket() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setStyle(Notification.BigTextStyle()) + .build() + + val entry = NotificationEntryBuilder().setNotification(notification).build() + entry.bucket = BUCKET_ALERTING + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.sectionBucket).isEqualTo(BUCKET_ALERTING) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isAmbient() { + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setImportance(NotificationManager.IMPORTANCE_MIN) + .build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.isAmbient).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun canShowFullScreen() { + val notification: Notification = + Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setFullScreenIntent(Mockito.mock(PendingIntent::class.java), true) + .build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setImportance(NotificationManager.IMPORTANCE_MIN) + .build() + + underTest = factory.create(entry) as NotificationEntryAdapter + assertThat(underTest.isFullScreenCapable).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun onNotificationBubbleIconClicked() { + val notification: Notification = + Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build() + + val entry = + NotificationEntryBuilder() + .setNotification(notification) + .setImportance(NotificationManager.IMPORTANCE_MIN) + .build() + + underTest = factory.create(entry) as NotificationEntryAdapter + + underTest.onNotificationBubbleIconClicked() + verify((factory as? EntryAdapterFactoryImpl)?.getNotificationActivityStarter()) + ?.onNotificationBubbleIconClicked(entry) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 481081344dbb..790b2c343a11 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -21,24 +21,17 @@ import static android.app.Notification.CATEGORY_CALL; import static android.app.Notification.CATEGORY_EVENT; import static android.app.Notification.CATEGORY_MESSAGE; import static android.app.Notification.CATEGORY_REMINDER; -import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED; import static android.app.Notification.FLAG_PROMOTED_ONGOING; -import static android.app.NotificationManager.IMPORTANCE_MIN; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; import static com.android.systemui.statusbar.NotificationEntryHelper.modifySbn; -import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING; - -import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.Notification; @@ -66,8 +59,6 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SbnBuilder; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; -import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; @@ -458,336 +449,6 @@ public class NotificationEntryTest extends SysuiTestCase { // no crash, good } - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void getParent_adapter() { - GroupEntry ge = new GroupEntryBuilder() - .build(); - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .setParent(ge) - .build(); - - assertThat(entry.getEntryAdapter().getParent()).isEqualTo(entry.getParent()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void isTopLevelEntry_adapter() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .setParent(GroupEntry.ROOT_ENTRY) - .build(); - - assertThat(entry.getEntryAdapter().isTopLevelEntry()).isTrue(); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void getKey_adapter() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .build(); - - assertThat(entry.getEntryAdapter().getKey()).isEqualTo(entry.getKey()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void getRow_adapter() { - ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .build(); - entry.setRow(row); - - assertThat(entry.getEntryAdapter().getRow()).isEqualTo(entry.getRow()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void isGroupRoot_adapter_groupSummary() { - ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setGroupSummary(true) - .setGroup("key") - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .setParent(GroupEntry.ROOT_ENTRY) - .build(); - entry.setRow(row); - - assertThat(entry.getEntryAdapter().isGroupRoot()).isFalse(); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void isGroupRoot_adapter_groupChild() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setGroupSummary(true) - .setGroup("key") - .build(); - - NotificationEntry parent = new NotificationEntryBuilder() - .setParent(GroupEntry.ROOT_ENTRY) - .build(); - GroupEntryBuilder groupEntry = new GroupEntryBuilder() - .setSummary(parent); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .setParent(groupEntry.build()) - .build(); - - assertThat(entry.getEntryAdapter().isGroupRoot()).isFalse(); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void isClearable_adapter() { - ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .build(); - entry.setRow(row); - - assertThat(entry.getEntryAdapter().isClearable()).isEqualTo(entry.isClearable()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void getSummarization_adapter() { - ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .build(); - Ranking ranking = new RankingBuilder(entry.getRanking()) - .setSummarization("hello") - .build(); - entry.setRanking(ranking); - entry.setRow(row); - - assertThat(entry.getEntryAdapter().getSummarization()).isEqualTo("hello"); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void getIcons_adapter() { - ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg(TEST_PACKAGE_NAME) - .setOpPkg(TEST_PACKAGE_NAME) - .setUid(TEST_UID) - .setChannel(mChannel) - .setId(mId++) - .setNotification(notification) - .setUser(new UserHandle(ActivityManager.getCurrentUser())) - .build(); - entry.setRow(row); - - assertThat(entry.getEntryAdapter().getIcons()).isEqualTo(entry.getIcons()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void isColorized() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setColorized(true) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setNotification(notification) - .build(); - assertThat(entry.getEntryAdapter().isColorized()).isEqualTo( - entry.getSbn().getNotification().isColorized()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void getSbn() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setNotification(notification) - .build(); - assertThat(entry.getEntryAdapter().getSbn()).isEqualTo( - entry.getSbn()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void canDragAndDrop() { - PendingIntent pi = mock(PendingIntent.class); - when(pi.isActivity()).thenReturn(true); - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setContentIntent(pi) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setNotification(notification) - .build(); - assertThat(entry.getEntryAdapter().canDragAndDrop()).isTrue(); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void isBubble() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setFlag(FLAG_BUBBLE, true) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setNotification(notification) - .build(); - assertThat(entry.getEntryAdapter().isBubbleCapable()).isEqualTo(entry.isBubble()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void getStyle() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setStyle(new Notification.BigTextStyle()) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setNotification(notification) - .build(); - assertThat(entry.getEntryAdapter().getStyle()).isEqualTo(entry.getNotificationStyle()); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void getSectionBucket() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setStyle(new Notification.BigTextStyle()) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setNotification(notification) - .build(); - entry.setBucket(BUCKET_ALERTING); - - assertThat(entry.getEntryAdapter().getSectionBucket()).isEqualTo(BUCKET_ALERTING); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void isAmbient() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setNotification(notification) - .setImportance(IMPORTANCE_MIN) - .build(); - - assertThat(entry.getEntryAdapter().isAmbient()).isTrue(); - } - - @Test - @EnableFlags(NotificationBundleUi.FLAG_NAME) - public void canShowFullScreen() { - Notification notification = new Notification.Builder(mContext, "") - .setSmallIcon(R.drawable.ic_person) - .setFullScreenIntent(mock(PendingIntent.class), true) - .build(); - - NotificationEntry entry = new NotificationEntryBuilder() - .setNotification(notification) - .setImportance(IMPORTANCE_MIN) - .build(); - - assertThat(entry.getEntryAdapter().isFullScreenCapable()).isTrue(); - } - private Notification.Action createContextualAction(String title) { return new Notification.Action.Builder( Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 609885d0214b..30983550f0f9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -549,7 +549,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTap() = + fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTapImmediately() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -570,8 +570,9 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { executor.runAllReady() beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) - // THEN HUN is hidden - verify(headsUpManager).removeNotification(eq(entry.key), eq(false), any()) + // THEN HUN is hidden and it's hidden immediately + verify(headsUpManager) + .removeNotification(eq(entry.key), /* releaseImmediately= */ eq(true), any()) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt index 8e6aedcae506..9e27c79d54b8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.notification.collection.render -import android.os.Build -import android.os.UserHandle import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule @@ -26,6 +24,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.assertLogsWtf +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactoryImpl import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.ListEntry @@ -36,12 +35,13 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationTestHelper +import com.android.systemui.statusbar.notification.row.entryAdapterFactory import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat -import org.junit.Assume import org.junit.Before import org.junit.Rule import org.junit.Test @@ -54,11 +54,11 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) class GroupExpansionManagerTest : SysuiTestCase() { - @get:Rule - val setFlagsRule = SetFlagsRule() + @get:Rule val setFlagsRule = SetFlagsRule() private lateinit var underTest: GroupExpansionManagerImpl + private val kosmos = testKosmos() private lateinit var testHelper: NotificationTestHelper private val dumpManager: DumpManager = mock() private val groupMembershipManager: GroupMembershipManager = mock() @@ -66,15 +66,14 @@ class GroupExpansionManagerTest : SysuiTestCase() { private val pipeline: NotifPipeline = mock() private lateinit var beforeRenderListListener: OnBeforeRenderListListener + private val factory: EntryAdapterFactoryImpl = kosmos.entryAdapterFactory private lateinit var summary1: NotificationEntry private lateinit var summary2: NotificationEntry private lateinit var entries: List<ListEntry> private fun notificationEntry(pkg: String, id: Int, parent: ExpandableNotificationRow?) = NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { - row = testHelper.createRow().apply { - setIsChildInGroup(true, parent) - } + row = testHelper.createRow().apply { setIsChildInGroup(true, parent) } } @Before @@ -91,7 +90,7 @@ class GroupExpansionManagerTest : SysuiTestCase() { listOf( notificationEntry("foo", 2, summary1.row), notificationEntry("foo", 3, summary1.row), - notificationEntry("foo", 4, summary1.row) + notificationEntry("foo", 4, summary1.row), ) ) .build(), @@ -101,11 +100,11 @@ class GroupExpansionManagerTest : SysuiTestCase() { listOf( notificationEntry("bar", 2, summary2.row), notificationEntry("bar", 3, summary2.row), - notificationEntry("bar", 4, summary2.row) + notificationEntry("bar", 4, summary2.row), ) ) .build(), - notificationEntry("baz", 1, null) + notificationEntry("baz", 1, null), ) whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1) @@ -135,18 +134,20 @@ class GroupExpansionManagerTest : SysuiTestCase() { @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun notifyOnlyOnChange_withEntryAdapter() { + val entryAdapter1 = factory.create(summary1) + val entryAdapter2 = factory.create(summary2) var listenerCalledCount = 0 underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } - underTest.setGroupExpanded(summary1.entryAdapter, false) + underTest.setGroupExpanded(entryAdapter1, false) assertThat(listenerCalledCount).isEqualTo(0) - underTest.setGroupExpanded(summary1.entryAdapter, true) + underTest.setGroupExpanded(entryAdapter1, true) assertThat(listenerCalledCount).isEqualTo(1) - underTest.setGroupExpanded(summary2.entryAdapter, true) + underTest.setGroupExpanded(entryAdapter2, true) assertThat(listenerCalledCount).isEqualTo(2) - underTest.setGroupExpanded(summary1.entryAdapter, true) + underTest.setGroupExpanded(entryAdapter1, true) assertThat(listenerCalledCount).isEqualTo(2) - underTest.setGroupExpanded(summary2.entryAdapter, false) + underTest.setGroupExpanded(entryAdapter2, false) assertThat(listenerCalledCount).isEqualTo(3) } @@ -168,16 +169,17 @@ class GroupExpansionManagerTest : SysuiTestCase() { @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun expandUnattachedEntryAdapter() { + val entryAdapter = factory.create(summary1) // First, expand the entry when it is attached. - underTest.setGroupExpanded(summary1.entryAdapter, true) - assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue() + underTest.setGroupExpanded(entryAdapter, true) + assertThat(underTest.isGroupExpanded(entryAdapter)).isTrue() // Un-attach it, and un-expand it. NotificationEntryBuilder.setNewParent(summary1, null) - underTest.setGroupExpanded(summary1.entryAdapter, false) + underTest.setGroupExpanded(entryAdapter, false) // Expanding again should throw. - assertLogsWtf { underTest.setGroupExpanded(summary1.entryAdapter, true) } + assertLogsWtf { underTest.setGroupExpanded(entryAdapter, true) } } @Test @@ -207,6 +209,7 @@ class GroupExpansionManagerTest : SysuiTestCase() { @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun syncWithPipeline_withEntryAdapter() { + val entryAdapter = factory.create(summary1) underTest.attach(pipeline) beforeRenderListListener = withArgCaptor { verify(pipeline).addOnBeforeRenderListListener(capture()) @@ -219,7 +222,7 @@ class GroupExpansionManagerTest : SysuiTestCase() { verify(listener, never()).onGroupExpansionChange(any(), any()) // Expand one of the groups. - underTest.setGroupExpanded(summary1.entryAdapter, true) + underTest.setGroupExpanded(entryAdapter, true) verify(listener).onGroupExpansionChange(summary1.row, true) // Empty the pipeline list and verify that the group is no longer expanded. @@ -231,11 +234,15 @@ class GroupExpansionManagerTest : SysuiTestCase() { @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupExpanded() { - underTest.setGroupExpanded(summary1.entryAdapter, true) - - assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue(); - assertThat(underTest.isGroupExpanded( - (entries[0] as? GroupEntry)?.getChildren()?.get(0)?.entryAdapter)) - .isTrue(); + val entryAdapter = summary1.row.entryAdapter + underTest.setGroupExpanded(entryAdapter, true) + + assertThat(underTest.isGroupExpanded(entryAdapter)).isTrue() + assertThat( + underTest.isGroupExpanded( + (entries[0] as? GroupEntry)?.getChildren()?.get(0)?.row?.entryAdapter + ) + ) + .isTrue() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt index 2bbf094021cf..0ccf58507696 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt @@ -22,10 +22,13 @@ import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactoryImpl import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.row.entryAdapterFactory import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test @@ -35,8 +38,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class GroupMembershipManagerTest : SysuiTestCase() { - @get:Rule - val setFlagsRule = SetFlagsRule() + @get:Rule val setFlagsRule = SetFlagsRule() + + private val kosmos = testKosmos() + private val factory: EntryAdapterFactoryImpl = kosmos.entryAdapterFactory private var underTest = GroupMembershipManagerImpl() @@ -144,14 +149,14 @@ class GroupMembershipManagerTest : SysuiTestCase() { @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isChildEntryAdapterInGroup_topLevel() { val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() - assertThat(underTest.isChildInGroup(topLevelEntry.entryAdapter)).isFalse() + assertThat(underTest.isChildInGroup(factory.create(topLevelEntry))).isFalse() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isChildEntryAdapterInGroup_noParent() { val noParentEntry = NotificationEntryBuilder().setParent(null).build() - assertThat(underTest.isChildInGroup(noParentEntry.entryAdapter)).isFalse() + assertThat(underTest.isChildInGroup(factory.create(noParentEntry))).isFalse() } @Test @@ -165,7 +170,7 @@ class GroupMembershipManagerTest : SysuiTestCase() { .build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - assertThat(underTest.isChildInGroup(summary.entryAdapter)).isFalse() + assertThat(underTest.isChildInGroup(factory.create(summary))).isFalse() } @Test @@ -180,14 +185,14 @@ class GroupMembershipManagerTest : SysuiTestCase() { val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() - assertThat(underTest.isChildInGroup(entry.entryAdapter)).isTrue() + assertThat(underTest.isChildInGroup(factory.create(entry))).isTrue() } @Test @EnableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupRoot_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() - assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse() + assertThat(underTest.isGroupRoot(factory.create(entry))).isFalse() } @Test @@ -201,7 +206,7 @@ class GroupMembershipManagerTest : SysuiTestCase() { .build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - assertThat(underTest.isGroupRoot(summary.entryAdapter)).isTrue() + assertThat(underTest.isGroupRoot(factory.create(summary))).isTrue() } @Test @@ -216,6 +221,6 @@ class GroupMembershipManagerTest : SysuiTestCase() { val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() - assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse() + assertThat(underTest.isGroupRoot(factory.create(entry))).isFalse() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt index dc27859df421..a2e4a328697e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/AvalancheControllerTest.kt @@ -17,9 +17,10 @@ package com.android.systemui.statusbar.notification.headsup import android.app.Notification import android.os.Handler +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper.RunWithLooper -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.SysuiTestCase @@ -28,6 +29,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl @@ -53,12 +55,18 @@ import org.mockito.Mockito import org.mockito.invocation.InvocationOnMock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest @RunWithLooper -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @EnableFlags(NotificationThrottleHun.FLAG_NAME) -class AvalancheControllerTest : SysuiTestCase() { +class AvalancheControllerTest(val flags: FlagsParameterization) : SysuiTestCase() { + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + private val kosmos = testKosmos() // For creating mocks @@ -72,10 +80,10 @@ class AvalancheControllerTest : SysuiTestCase() { // For creating TestableHeadsUpManager @Mock private val mAccessibilityMgr: AccessibilityManagerWrapper? = null private val mUiEventLoggerFake = UiEventLoggerFake() - @Mock private lateinit var mHeadsUpManagerLogger: HeadsUpManagerLogger + private val headsUpManagerLogger = HeadsUpManagerLogger(logcatLogBuffer()) @Mock private lateinit var mBgHandler: Handler - private val mLogger = Mockito.spy(HeadsUpManagerLogger(logcatLogBuffer())) + private val mLogger = Mockito.spy(headsUpManagerLogger) private val mGlobalSettings = FakeGlobalSettings() private val mSystemClock = FakeSystemClock() private val mExecutor = FakeExecutor(mSystemClock) @@ -95,7 +103,7 @@ class AvalancheControllerTest : SysuiTestCase() { // Initialize AvalancheController and TestableHeadsUpManager during setUp instead of // declaration, where mocks are null mAvalancheController = - AvalancheController(dumpManager, mUiEventLoggerFake, mHeadsUpManagerLogger, mBgHandler) + AvalancheController(dumpManager, mUiEventLoggerFake, headsUpManagerLogger, mBgHandler) testableHeadsUpManager = HeadsUpManagerImpl( @@ -278,7 +286,7 @@ class AvalancheControllerTest : SysuiTestCase() { // Delete mAvalancheController.delete(firstEntry, runnableMock, "testLabel") - // Next entry is shown + // Showing entry becomes previous assertThat(mAvalancheController.previousHunKey).isEqualTo(firstEntry.mEntry!!.key) } @@ -296,12 +304,12 @@ class AvalancheControllerTest : SysuiTestCase() { // Delete mAvalancheController.delete(showingEntry, runnableMock!!, "testLabel") - // Next entry is shown + // Previous key not filled in assertThat(mAvalancheController.previousHunKey).isEqualTo("") } @Test - fun testGetDurationMs_untrackedEntryEmptyAvalanche_useAutoDismissTime() { + fun testGetDuration_untrackedEntryEmptyAvalanche_useAutoDismissTime() { val givenEntry = createHeadsUpEntry(id = 0) // Nothing is showing @@ -310,12 +318,12 @@ class AvalancheControllerTest : SysuiTestCase() { // Nothing is next mAvalancheController.clearNext() - val durationMs = mAvalancheController.getDurationMs(givenEntry, autoDismissMs = 5000) - assertThat(durationMs).isEqualTo(5000) + val durationMs = mAvalancheController.getDuration(givenEntry, autoDismissMsValue = 5000) + assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(5000) } @Test - fun testGetDurationMs_untrackedEntryNonEmptyAvalanche_useAutoDismissTime() { + fun testGetDuration_untrackedEntryNonEmptyAvalanche_useAutoDismissTime() { val givenEntry = createHeadsUpEntry(id = 0) // Given entry not tracked @@ -325,12 +333,12 @@ class AvalancheControllerTest : SysuiTestCase() { val nextEntry = createHeadsUpEntry(id = 2) mAvalancheController.addToNext(nextEntry, runnableMock!!) - val durationMs = mAvalancheController.getDurationMs(givenEntry, autoDismissMs = 5000) - assertThat(durationMs).isEqualTo(5000) + val durationMs = mAvalancheController.getDuration(givenEntry, autoDismissMsValue = 5000) + assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(5000) } @Test - fun testGetDurationMs_lastEntry_useAutoDismissTime() { + fun testGetDuration_lastEntry_useAutoDismissTime() { // Entry is showing val showingEntry = createHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry @@ -338,12 +346,12 @@ class AvalancheControllerTest : SysuiTestCase() { // Nothing is next mAvalancheController.clearNext() - val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000) - assertThat(durationMs).isEqualTo(5000) + val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) + assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(5000) } @Test - fun testGetDurationMs_nextEntryLowerPriority_5000() { + fun testGetDuration_nextEntryLowerPriority_5000() { // Entry is showing val showingEntry = createFsiHeadsUpEntry(id = 1) mAvalancheController.headsUpEntryShowing = showingEntry @@ -355,12 +363,12 @@ class AvalancheControllerTest : SysuiTestCase() { // Next entry has lower priority assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(1) - val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000) - assertThat(durationMs).isEqualTo(5000) + val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) + assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(5000) } @Test - fun testGetDurationMs_nextEntrySamePriority_1000() { + fun testGetDuration_nextEntrySamePriority_1000() { // Entry is showing val showingEntry = createHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry @@ -372,12 +380,12 @@ class AvalancheControllerTest : SysuiTestCase() { // Same priority assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(0) - val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000) - assertThat(durationMs).isEqualTo(1000) + val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) + assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(1000) } @Test - fun testGetDurationMs_nextEntryHigherPriority_500() { + fun testGetDuration_nextEntryHigherPriority_500() { // Entry is showing val showingEntry = createHeadsUpEntry(id = 0) mAvalancheController.headsUpEntryShowing = showingEntry @@ -389,7 +397,51 @@ class AvalancheControllerTest : SysuiTestCase() { // Next entry has higher priority assertThat(nextEntry.compareNonTimeFields(showingEntry)).isEqualTo(-1) - val durationMs = mAvalancheController.getDurationMs(showingEntry, autoDismissMs = 5000) - assertThat(durationMs).isEqualTo(500) + val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) + assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(500) + } + + @Test + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + fun testGetDuration_nextEntryIsPinnedByUser_flagOff_1000() { + // Entry is showing + val showingEntry = createHeadsUpEntry(id = 0) + mAvalancheController.headsUpEntryShowing = showingEntry + + // There's another entry waiting to show next and it's PinnedByUser + val nextEntry = createHeadsUpEntry(id = 1) + nextEntry.requestedPinnedStatus = PinnedStatus.PinnedByUser + mAvalancheController.addToNext(nextEntry, runnableMock!!) + + val durationMs = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) + + // BUT PinnedByUser is ignored because flag is off, so the duration for a SAME priority next + // is used + assertThat((durationMs as RemainingDuration.UpdatedDuration).duration).isEqualTo(1000) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun testGetDuration_nextEntryIsPinnedByUser_flagOn_hideImmediately() { + // Entry is showing + val showingEntry = createHeadsUpEntry(id = 0) + mAvalancheController.headsUpEntryShowing = showingEntry + + // There's another entry waiting to show next and it's PinnedByUser + val nextEntry = createHeadsUpEntry(id = 1) + nextEntry.requestedPinnedStatus = PinnedStatus.PinnedByUser + mAvalancheController.addToNext(nextEntry, runnableMock!!) + + val duration = mAvalancheController.getDuration(showingEntry, autoDismissMsValue = 5000) + + assertThat(duration).isEqualTo(RemainingDuration.HideImmediately) + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(StatusBarNotifChips.FLAG_NAME) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimatorTest.kt index 206eb89db94f..706885bf5dee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimatorTest.kt @@ -21,6 +21,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R +import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.Before @@ -30,6 +32,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME) class HeadsUpAnimatorTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Before fun setUp() { context.getOrCreateTestableResources().apply { @@ -38,34 +42,64 @@ class HeadsUpAnimatorTest : SysuiTestCase() { } @Test - fun getHeadsUpYTranslation_fromBottomTrue_usesBottomAndYAbove() { - val underTest = HeadsUpAnimator(context) + fun getHeadsUpYTranslation_fromBottomTrue_hasStatusBarChipFalse_usesBottomAndYAbove() { + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) + underTest.stackTopMargin = 30 + underTest.headsUpAppearHeightBottom = 300 + + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = false) + + assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300) + } + + @Test + fun getHeadsUpYTranslation_fromBottomTrue_hasStatusBarChipTrue_usesBottomAndYAbove() { + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) underTest.stackTopMargin = 30 underTest.headsUpAppearHeightBottom = 300 - val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true) + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = true) + // fromBottom takes priority assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300) } @Test - fun getHeadsUpYTranslation_fromBottomFalse_usesTopMarginAndYAbove() { - val underTest = HeadsUpAnimator(context) + fun getHeadsUpYTranslation_fromBottomFalse_hasStatusBarChipFalse_usesTopMarginAndYAbove() { + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) underTest.stackTopMargin = 30 underTest.headsUpAppearHeightBottom = 300 - val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false) + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false, hasStatusBarChip = false) assertThat(yTranslation).isEqualTo(-30 - TEST_Y_ABOVE_SCREEN) } @Test + fun getHeadsUpYTranslation_fromBottomFalse_hasStatusBarChipTrue_usesTopMarginAndStatusBarHeight() { + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) + underTest.stackTopMargin = 30 + underTest.headsUpAppearHeightBottom = 300 + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = 75 + underTest.updateResources(context) + + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = false, hasStatusBarChip = true) + + assertThat(yTranslation).isEqualTo(75 - 30) + } + + @Test fun getHeadsUpYTranslation_resourcesUpdated() { - val underTest = HeadsUpAnimator(context) + val underTest = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) underTest.stackTopMargin = 30 underTest.headsUpAppearHeightBottom = 300 - val yTranslation = underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true) + val yTranslation = + underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true, hasStatusBarChip = false) assertThat(yTranslation).isEqualTo(TEST_Y_ABOVE_SCREEN + 300) @@ -77,7 +111,12 @@ class HeadsUpAnimatorTest : SysuiTestCase() { underTest.updateResources(context) // THEN HeadsUpAnimator knows about it - assertThat(underTest.getHeadsUpYTranslation(isHeadsUpFromBottom = true)) + assertThat( + underTest.getHeadsUpYTranslation( + isHeadsUpFromBottom = true, + hasStatusBarChip = false, + ) + ) .isEqualTo(newYAbove + 300) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt index f060caea4f24..bb12eff51288 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt @@ -37,9 +37,12 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.statusbar.SmartReplyController import com.android.systemui.statusbar.notification.ColorUpdateLogger +import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.EntryAdapter +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactory import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider import com.android.systemui.statusbar.notification.collection.render.FakeNodeController import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager @@ -47,6 +50,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController.BUBBLES_SETTING_URI +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger import com.android.systemui.statusbar.notification.stack.NotificationListContainer @@ -112,6 +116,7 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { private val uiEventLogger: UiEventLogger = mock() private val msdlPlayer: MSDLPlayer = mock() private val rebindingTracker: NotificationRebindingTracker = mock() + private val entryAdapterFactory: EntryAdapterFactory = mock() private lateinit var controller: ExpandableNotificationRowController @Before @@ -154,6 +159,7 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { uiEventLogger, msdlPlayer, rebindingTracker, + entryAdapterFactory, ) whenever(view.childrenContainer).thenReturn(childrenContainer) @@ -277,8 +283,10 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { whenever(view.privateLayout).thenReturn(childView) val entryAdapter = mock(EntryAdapter::class.java) val sbn = - SbnBuilder().setNotification(Notification.Builder(mContext).build()) - .setUser(UserHandle.of(view.entry.sbn.userId)).build() + SbnBuilder() + .setNotification(Notification.Builder(mContext).build()) + .setUser(UserHandle.of(view.entry.sbn.userId)) + .build() whenever(entryAdapter.sbn).thenReturn(sbn) whenever(view.entryAdapter).thenReturn(entryAdapter) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index c376fad1503f..8e7733bb23ca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -70,10 +70,14 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; +import com.android.systemui.statusbar.notification.NotificationActivityStarter; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactoryImpl; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; @@ -87,6 +91,7 @@ import com.android.systemui.statusbar.notification.promoted.FakePromotedNotifica import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -101,11 +106,6 @@ import com.android.systemui.util.time.SystemClock; import com.android.systemui.util.time.SystemClockImpl; import com.android.systemui.wmshell.BubblesTestActivity; -import kotlin.coroutines.CoroutineContext; - -import kotlinx.coroutines.flow.StateFlowKt; -import kotlinx.coroutines.test.TestScope; - import org.mockito.ArgumentCaptor; import java.util.List; @@ -114,6 +114,10 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import kotlin.coroutines.CoroutineContext; +import kotlinx.coroutines.flow.StateFlowKt; +import kotlinx.coroutines.test.TestScope; + /** * A helper class to create {@link ExpandableNotificationRow} (for both individual and group * notifications). @@ -734,7 +738,16 @@ public class NotificationTestHelper { mBindPipelineEntryListener.onEntryInit(entry); mBindPipeline.manageRow(entry, row); + EntryAdapter entryAdapter = new EntryAdapterFactoryImpl( + mock(NotificationActivityStarter.class), + mock(MetricsLogger.class), + mock(PeopleNotificationIdentifier.class), + mock(NotificationIconStyleProvider.class), + mock(VisualStabilityCoordinator.class) + ).create(entry); + row.initialize( + entryAdapter, entry, mock(RemoteInputViewSubcomponent.Factory.class), APP_NAME, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 14bbd38ece2c..761ed6186afc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -27,6 +27,7 @@ import android.view.NotificationHeaderView; import android.view.View; import android.widget.RemoteViews; +import androidx.compose.ui.platform.ComposeView; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -35,7 +36,9 @@ import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModelImpl; import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import org.junit.Assert; import org.junit.Before; @@ -273,6 +276,22 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); } + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void initBundleHeader_composeview_is_initialized_once() { + View currentView = mChildrenContainer.getChildAt(mChildrenContainer.getChildCount() - 1); + Assert.assertFalse(currentView instanceof ComposeView); + + BundleHeaderViewModelImpl viewModel = new BundleHeaderViewModelImpl(); + mChildrenContainer.initBundleHeader(viewModel); + currentView = mChildrenContainer.getChildAt(mChildrenContainer.getChildCount() - 1); + Assert.assertTrue(currentView instanceof ComposeView); + + mChildrenContainer.initBundleHeader(viewModel); + View finalView = mChildrenContainer.getChildAt(mChildrenContainer.getChildCount() - 1); + Assert.assertEquals(currentView, finalView); + } + private NotificationHeaderView createHeaderView(boolean lowPriority) { Notification notification = mNotificationTestHelper.createNotification(); final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 954515015fd9..08ecbac1582c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.statusbar.notification.stack import android.annotation.DimenRes import android.content.pm.PackageManager +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.widget.FrameLayout import androidx.test.filters.SmallTest @@ -19,6 +20,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.RoundableState import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -32,6 +34,8 @@ import com.android.systemui.statusbar.notification.headsup.NotificationsHunShare import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy +import com.android.systemui.testKosmos import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat import org.junit.Assume @@ -53,6 +57,8 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { @JvmField @Rule var expect: Expect = Expect.create() + private val kosmos = testKosmos() + private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>() private val avalancheController = mock<AvalancheController>() @@ -131,13 +137,14 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { hostView.addView(notificationRow) if (NotificationsHunSharedAnimationValues.isEnabled) { - headsUpAnimator = HeadsUpAnimator(context) + headsUpAnimator = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) } - stackScrollAlgorithm = StackScrollAlgorithm( - context, - hostView, - if (::headsUpAnimator.isInitialized) headsUpAnimator else null, - ) + stackScrollAlgorithm = + StackScrollAlgorithm( + context, + hostView, + if (::headsUpAnimator.isInitialized) headsUpAnimator else null, + ) } private fun isTv(): Boolean { @@ -450,6 +457,46 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun resetViewStates_hunAnimatingAway_noStatusBarChip_hunTranslatedToTopOfScreen() { + val topMargin = 100f + ambientState.maxHeadsUpTranslation = 2000f + ambientState.stackTopMargin = topMargin.toInt() + headsUpAnimator?.stackTopMargin = topMargin.toInt() + whenever(notificationRow.intrinsicHeight).thenReturn(100) + + val statusBarHeight = 432 + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight + headsUpAnimator!!.updateResources(context) + + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + whenever(notificationRow.hasStatusBarChipDuringHeadsUpAnimation()).thenReturn(false) + + resetViewStates_hunYTranslationIs( + expected = -topMargin - stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen + ) + } + + @Test + @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun resetViewStates_hunAnimatingAway_withStatusBarChip_hunTranslatedToBottomOfStatusBar() { + val topMargin = 100f + ambientState.maxHeadsUpTranslation = 2000f + ambientState.stackTopMargin = topMargin.toInt() + headsUpAnimator?.stackTopMargin = topMargin.toInt() + whenever(notificationRow.intrinsicHeight).thenReturn(100) + + val statusBarHeight = 432 + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight + headsUpAnimator!!.updateResources(context) + + whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) + whenever(notificationRow.hasStatusBarChipDuringHeadsUpAnimation()).thenReturn(true) + + resetViewStates_hunYTranslationIs(expected = statusBarHeight - topMargin) + } + + @Test fun resetViewStates_hunAnimatingAway_bottomNotClipped() { whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt index cb4642cc21be..f6c031f54818 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt @@ -26,12 +26,15 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.AnimatorTestRule import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_APPEAR import com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_HEADS_UP_DISAPPEAR +import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -46,7 +49,6 @@ import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.description import org.mockito.Mockito.eq import org.mockito.Mockito.verify -import org.mockito.kotlin.doNothing private const val VIEW_HEIGHT = 100 private const val FULL_SHADE_APPEAR_TRANSLATION = 300 @@ -60,6 +62,8 @@ class StackStateAnimatorTest : SysuiTestCase() { @get:Rule val setFlagsRule = SetFlagsRule() @get:Rule val animatorTestRule = AnimatorTestRule(this) + private val kosmos = testKosmos() + private lateinit var stackStateAnimator: StackStateAnimator private lateinit var headsUpAnimator: HeadsUpAnimator private val stackScroller: NotificationStackScrollLayout = mock() @@ -80,13 +84,14 @@ class StackStateAnimatorTest : SysuiTestCase() { whenever(view.viewState).thenReturn(viewState) if (NotificationsHunSharedAnimationValues.isEnabled) { - headsUpAnimator = HeadsUpAnimator(context) + headsUpAnimator = HeadsUpAnimator(context, kosmos.fakeSystemBarUtilsProxy) } - stackStateAnimator = StackStateAnimator( - mContext, - stackScroller, - if (::headsUpAnimator.isInitialized) headsUpAnimator else null, - ) + stackStateAnimator = + StackStateAnimator( + mContext, + stackScroller, + if (::headsUpAnimator.isInitialized) headsUpAnimator else null, + ) } @Test @@ -134,6 +139,62 @@ class StackStateAnimatorTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun startAnimationForEvents_headsUpFromTop_andHasStatusBarChipFalse() { + val statusBarHeight = 156 + val topMargin = 50f + val expectedStartY = -topMargin - HEADS_UP_ABOVE_SCREEN + + headsUpAnimator.stackTopMargin = topMargin.toInt() + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight + headsUpAnimator.updateResources(context) + + val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) + event.headsUpHasStatusBarChip = false + + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view).setFinalActualHeight(VIEW_HEIGHT) + verify(view, description("should animate from the top")).translationY = expectedStartY + verify(view) + .performAddAnimation( + /* delay= */ 0L, + /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), + /* isHeadsUpAppear= */ true, + /* isHeadsUpCycling= */ false, + /* onEndRunnable= */ null, + ) + } + + @Test + @EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun startAnimationForEvents_headsUpFromTop_andHasStatusBarChipTrue() { + val statusBarHeight = 156 + val topMargin = 50f + val expectedStartY = statusBarHeight - topMargin + + headsUpAnimator!!.stackTopMargin = topMargin.toInt() + kosmos.fakeSystemBarUtilsProxy.fakeStatusBarHeight = statusBarHeight + headsUpAnimator!!.updateResources(context) + + val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR) + event.headsUpHasStatusBarChip = true + + stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) + + verify(view).setFinalActualHeight(VIEW_HEIGHT) + verify(view, description("should animate below status bar")).translationY = expectedStartY + verify(view) + .performAddAnimation( + /* delay= */ 0L, + /* duration= */ ANIMATION_DURATION_HEADS_UP_APPEAR.toLong(), + /* isHeadsUpAppear= */ true, + /* isHeadsUpCycling= */ false, + /* onEndRunnable= */ null, + ) + } + + @Test @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME) fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim_flagOff() { val screenHeight = 2000f diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index 31f8590c0378..46430afecbb1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt @@ -70,6 +70,7 @@ import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -83,6 +84,7 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -155,7 +157,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { testScope = kosmos.testScope shadeViewStateProvider = TestShadeViewStateProvider() - Mockito.`when`( + whenever( kosmos.mockStatusBarContentInsetsProvider .getStatusBarContentInsetsForCurrentRotation() ) @@ -163,9 +165,9 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) - Mockito.`when`(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any())) + whenever(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(iconManager) - Mockito.`when`(statusBarContentInsetsProviderStore.forDisplay(context.displayId)) + whenever(statusBarContentInsetsProviderStore.forDisplay(context.displayId)) .thenReturn(kosmos.mockStatusBarContentInsetsProvider) allowTestableLooperAsMainThread() looper.runWithLooper { @@ -174,7 +176,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { LayoutInflater.from(mContext).inflate(R.layout.keyguard_status_bar, null) as KeyguardStatusBarView ) - Mockito.`when`(keyguardStatusBarView.getDisplay()).thenReturn(mContext.display) + whenever(keyguardStatusBarView.display).thenReturn(mContext.display) } controller = createController() @@ -404,14 +406,14 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { fun updateViewState_alphaAndVisibilityGiven_viewUpdated() { // Verify the initial values so we know the method triggers changes. Truth.assertThat(keyguardStatusBarView.alpha).isEqualTo(1f) - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) val newAlpha = 0.5f val newVisibility = View.INVISIBLE controller.updateViewState(newAlpha, newVisibility) Truth.assertThat(keyguardStatusBarView.alpha).isEqualTo(newAlpha) - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(newVisibility) + assertThat(keyguardStatusBarView.visibility).isEqualTo(newVisibility) } @Test @@ -423,7 +425,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState(1f, View.VISIBLE) // Since we're disabled, we stay invisible - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test @@ -444,15 +446,15 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { fun updateViewState_bypassEnabledAndShouldListenForFace_viewHidden() { controller.onViewAttached() updateStateToKeyguard() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) - Mockito.`when`(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true) - Mockito.`when`(keyguardBypassController.bypassEnabled).thenReturn(true) + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true) + whenever(keyguardBypassController.bypassEnabled).thenReturn(true) onFinishedGoingToSleep() controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test @@ -461,13 +463,13 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.onViewAttached() updateStateToKeyguard() - Mockito.`when`(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true) - Mockito.`when`(keyguardBypassController.bypassEnabled).thenReturn(false) + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true) + whenever(keyguardBypassController.bypassEnabled).thenReturn(false) onFinishedGoingToSleep() controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) } @Test @@ -476,13 +478,13 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.onViewAttached() updateStateToKeyguard() - Mockito.`when`(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(false) - Mockito.`when`(keyguardBypassController.bypassEnabled).thenReturn(true) + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(false) + whenever(keyguardBypassController.bypassEnabled).thenReturn(true) onFinishedGoingToSleep() controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) } @Test @@ -495,7 +497,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test @@ -508,7 +510,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test @@ -520,7 +522,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) } @Test @@ -532,7 +534,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test @@ -544,7 +546,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) } @Test @@ -556,7 +558,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test @@ -568,7 +570,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.setDozing(true) - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test @@ -580,7 +582,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.setDozing(false) - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) } @Test @@ -595,7 +597,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.GONE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.GONE) Truth.assertThat(keyguardStatusBarView.alpha).isEqualTo(0.456f) } @@ -611,7 +613,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.updateViewState(0.789f, View.VISIBLE) - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.GONE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.GONE) Truth.assertThat(keyguardStatusBarView.alpha).isEqualTo(0.456f) } @@ -635,13 +637,13 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.init() controller.onViewAttached() updateStateToKeyguard() - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) controller.setDozing(true) // setDozing(true) should typically cause the view to hide. But since the flag is on, we // should ignore these set dozing calls and stay the same visibility. - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) } @Test @@ -679,7 +681,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { shadeViewStateProvider.setShouldHeadsUpBeVisible(true) controller.updateForHeadsUp(/* animate= */ false) - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test @@ -695,7 +697,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { shadeViewStateProvider.setShouldHeadsUpBeVisible(false) controller.updateForHeadsUp(/* animate= */ false) - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.VISIBLE) } @Test @@ -728,7 +730,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { val str = mContext.getString(com.android.internal.R.string.status_bar_volume) // GIVEN the setting is off - Mockito.`when`(secureSettings.getInt(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON, 0)) + whenever(secureSettings.getInt(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON, 0)) .thenReturn(0) // WHEN CollapsedStatusBarFragment builds the blocklist @@ -744,7 +746,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { val str = mContext.getString(com.android.internal.R.string.status_bar_volume) // GIVEN the setting is ON - Mockito.`when`( + whenever( secureSettings.getIntForUser( Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON, 0, @@ -779,42 +781,52 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { controller.onViewAttached() updateStateToKeyguard() setDisableSystemInfo(true) - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) controller.animateKeyguardStatusBarIn() // Since we're disabled, we don't actually animate in and stay invisible - Truth.assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) + assertThat(keyguardStatusBarView.visibility).isEqualTo(View.INVISIBLE) } @Test fun animateToGlanceableHub_affectsAlpha() = testScope.runTest { - controller.init() - val transitionAlphaAmount = .5f - ViewUtils.attachView(keyguardStatusBarView) - looper.processAllMessages() - updateStateToKeyguard() - kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Communal) - runCurrent() - controller.updateCommunalAlphaTransition(transitionAlphaAmount) - Truth.assertThat(keyguardStatusBarView.getAlpha()).isEqualTo(transitionAlphaAmount) + try { + controller.init() + val transitionAlphaAmount = .5f + ViewUtils.attachView(keyguardStatusBarView) + + looper.processAllMessages() + updateStateToKeyguard() + kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Communal) + runCurrent() + controller.updateCommunalAlphaTransition(transitionAlphaAmount) + assertThat(keyguardStatusBarView.getAlpha()).isEqualTo(transitionAlphaAmount) + } finally { + ViewUtils.detachView(keyguardStatusBarView) + } } @Test fun animateToGlanceableHub_alphaResetOnCommunalNotShowing() = testScope.runTest { - controller.init() - val transitionAlphaAmount = .5f - ViewUtils.attachView(keyguardStatusBarView) - looper.processAllMessages() - updateStateToKeyguard() - kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Communal) - runCurrent() - controller.updateCommunalAlphaTransition(transitionAlphaAmount) - kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Blank) - runCurrent() - Truth.assertThat(keyguardStatusBarView.getAlpha()).isNotEqualTo(transitionAlphaAmount) + try { + controller.init() + val transitionAlphaAmount = .5f + ViewUtils.attachView(keyguardStatusBarView) + + looper.processAllMessages() + updateStateToKeyguard() + kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Communal) + runCurrent() + controller.updateCommunalAlphaTransition(transitionAlphaAmount) + kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Blank) + runCurrent() + assertThat(keyguardStatusBarView.getAlpha()).isNotEqualTo(transitionAlphaAmount) + } finally { + ViewUtils.detachView(keyguardStatusBarView) + } } /** diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt index 47967b3f0828..670c195ee5a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.commandQueue import com.android.systemui.statusbar.lockscreenShadeTransitionController +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactoryImpl import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.domain.interactor.notificationAlertsInteractor @@ -54,6 +55,7 @@ import com.android.systemui.statusbar.notification.interruption.VisualInterrupti import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.entryAdapterFactory import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController import com.android.systemui.statusbar.notification.visualInterruptionDecisionProvider @@ -105,10 +107,14 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { private val notificationAlertsInteractor = kosmos.notificationAlertsInteractor private val visualInterruptionDecisionProvider = kosmos.visualInterruptionDecisionProvider + private lateinit var factory: EntryAdapterFactoryImpl + private lateinit var underTest: StatusBarNotificationPresenter @Before fun setup() { + factory = kosmos.entryAdapterFactory + underTest = createPresenter() if (VisualInterruptionRefactor.isEnabled) { verifyAndCaptureSuppressors() @@ -451,7 +457,7 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { private fun createRow(entry: NotificationEntry): ExpandableNotificationRow { val row: ExpandableNotificationRow = mock() if (NotificationBundleUi.isEnabled) { - whenever(row.entryAdapter).thenReturn(entry.entryAdapter) + whenever(row.entryAdapter).thenReturn(factory.create(entry)) } else { whenever(row.entry).thenReturn(entry) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java index 345ddae42798..c23e0e733b41 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java @@ -46,7 +46,7 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntry.NotifEntryAdapter; +import com.android.systemui.statusbar.notification.collection.NotificationEntryAdapter; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationContentView; @@ -135,7 +135,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class); final NotificationContentView privateLayout = mock(NotificationContentView.class); final NotificationEntry enrEntry = mock(NotificationEntry.class); - final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class); + final NotificationEntryAdapter enrEntryAdapter = mock(NotificationEntryAdapter.class); when(enr.getPrivateLayout()).thenReturn(privateLayout); when(enr.getEntry()).thenReturn(enrEntry); @@ -178,7 +178,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { // THEN verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntryAdapter.class)); verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -203,7 +203,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { // THEN verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntryAdapter.class)); verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -217,7 +217,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class); final NotificationContentView privateLayout = mock(NotificationContentView.class); final NotificationEntry enrEntry = mock(NotificationEntry.class); - final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class); + final NotificationEntryAdapter enrEntryAdapter = mock(NotificationEntryAdapter.class); when(enr.getPrivateLayout()).thenReturn(privateLayout); when(enr.getEntry()).thenReturn(enrEntry); @@ -260,7 +260,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { // THEN verify(mGroupExpansionManager, never()).toggleGroupExpansion(enrEntry); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntryAdapter.class)); verify(enr, never()).setUserExpanded(anyBoolean()); verify(privateLayout, never()).setOnExpandedVisibleListener(any()); } @@ -290,7 +290,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntryAdapter.class)); } @Test @@ -318,7 +318,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntryAdapter.class)); } @Test @@ -346,7 +346,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntryAdapter.class)); } @Test @@ -374,6 +374,6 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntryAdapter.class)); } } 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 f91e3a612862..91b3896332f5 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 @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.model.ChipsVisibilityModel import com.android.systemui.statusbar.pipeline.shared.ui.model.SystemInfoCombinedVisibilityModel import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel import kotlinx.coroutines.flow.Flow @@ -52,7 +53,10 @@ class FakeHomeStatusBarViewModel( override val primaryOngoingActivityChip: MutableStateFlow<OngoingActivityChipModel> = MutableStateFlow(OngoingActivityChipModel.Inactive()) - override val ongoingActivityChips = MutableStateFlow(MultipleOngoingActivityChipsModel()) + override val ongoingActivityChips = + MutableStateFlow( + ChipsVisibilityModel(MultipleOngoingActivityChipsModel(), areChipsAllowed = false) + ) override val ongoingActivityChipsLegacy = MutableStateFlow(MultipleOngoingActivityChipsModelLegacy()) @@ -62,7 +66,7 @@ class FakeHomeStatusBarViewModel( override val mediaProjectionStopDialogDueToCallEndedState = MutableStateFlow(MediaProjectionStopDialogModel.Hidden) - override val isHomeStatusBarAllowedByScene = MutableStateFlow(false) + override val isHomeStatusBarAllowed = MutableStateFlow(false) override val canShowOngoingActivityChips: Flow<Boolean> = 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 7e8ee1b156df..12cf3b6cd2cf 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 @@ -30,6 +30,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.View 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.display.data.repository.displayRepository import com.android.systemui.display.data.repository.fake @@ -57,6 +58,7 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository +import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository 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 @@ -64,6 +66,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaPr 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.assertIsCallChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip import com.android.systemui.statusbar.core.StatusBarRootModernization @@ -88,6 +91,7 @@ import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher import com.android.systemui.statusbar.phone.data.repository.fakeDarkIconRepository import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState import com.android.systemui.statusbar.pipeline.shared.domain.interactor.setHomeStatusBarIconBlockList import com.android.systemui.statusbar.pipeline.shared.domain.interactor.setHomeStatusBarInteractorShowOperatorName import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel @@ -95,7 +99,6 @@ import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runCurrent import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -500,9 +503,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isHomeStatusBarAllowedByScene_sceneLockscreen_notOccluded_false() = + @EnableSceneContainer + fun isHomeStatusBarAllowed_sceneLockscreen_notOccluded_false() = kosmos.runTest { - val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false, taskInfo = null) @@ -511,9 +515,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isHomeStatusBarAllowedByScene_sceneLockscreen_occluded_true() = + @EnableSceneContainer + fun isHomeStatusBarAllowed_sceneLockscreen_occluded_true() = kosmos.runTest { - val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, taskInfo = null) @@ -522,9 +527,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isHomeStatusBarAllowedByScene_overlayBouncer_false() = + @EnableSceneContainer + fun isHomeStatusBarAllowed_overlayBouncer_false() = kosmos.runTest { - val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) kosmos.sceneContainerRepository.showOverlay(Overlays.Bouncer) @@ -533,9 +539,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isHomeStatusBarAllowedByScene_sceneCommunal_false() = + @EnableSceneContainer + fun isHomeStatusBarAllowed_sceneCommunal_false() = kosmos.runTest { - val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) kosmos.sceneContainerRepository.snapToScene(Scenes.Communal) @@ -543,9 +550,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isHomeStatusBarAllowedByScene_sceneShade_false() = + @EnableSceneContainer + fun isHomeStatusBarAllowed_sceneShade_false() = kosmos.runTest { - val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) kosmos.sceneContainerRepository.snapToScene(Scenes.Shade) @@ -553,9 +561,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isHomeStatusBarAllowedByScene_sceneGone_true() = + @EnableSceneContainer + fun isHomeStatusBarAllowed_sceneGone_true() = kosmos.runTest { - val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) kosmos.sceneContainerRepository.snapToScene(Scenes.Gone) @@ -563,9 +572,10 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isHomeStatusBarAllowedByScene_sceneGoneWithNotificationsShadeOverlay_false() = + @EnableSceneContainer + fun isHomeStatusBarAllowed_sceneGoneWithNotificationsShadeOverlay_false() = kosmos.runTest { - val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) kosmos.sceneContainerRepository.snapToScene(Scenes.Gone) kosmos.sceneContainerRepository.showOverlay(Overlays.NotificationsShade) @@ -575,14 +585,104 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun isHomeStatusBarAllowedByScene_sceneGoneWithQuickSettingsShadeOverlay_false() = + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + @EnableSceneContainer + fun isHomeStatusBarAllowed_QsVisibleButInExternalDisplay_defaultStatusBarVisible() = + kosmos.runTest { + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) + + kosmos.sceneContainerRepository.snapToScene(Scenes.Gone) + kosmos.sceneContainerRepository.showOverlay(Overlays.QuickSettingsShade) + kosmos.fakeShadeDisplaysRepository.setDisplayId(EXTERNAL_DISPLAY) + runCurrent() + + assertThat(latest).isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + @EnableSceneContainer + fun isHomeStatusBarAllowed_QsVisibleButInExternalDisplay_withFlagOff_defaultStatusBarInvisible() = + kosmos.runTest { + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) + + kosmos.sceneContainerRepository.snapToScene(Scenes.Gone) + kosmos.sceneContainerRepository.showOverlay(Overlays.QuickSettingsShade) + kosmos.fakeShadeDisplaysRepository.setDisplayId(EXTERNAL_DISPLAY) + runCurrent() + + // Shade position is ignored. + assertThat(latest).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + @EnableSceneContainer + fun isHomeStatusBarAllowed_qsVisibleInThisDisplay_thisStatusBarInvisible() = + kosmos.runTest { + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) + + kosmos.sceneContainerRepository.snapToScene(Scenes.Gone) + kosmos.sceneContainerRepository.showOverlay(Overlays.QuickSettingsShade) + kosmos.fakeShadeDisplaysRepository.setDisplayId(DEFAULT_DISPLAY) + runCurrent() + + assertThat(latest).isFalse() + } + + @Test + @EnableSceneContainer + fun isHomeStatusBarAllowed_qsExpandedOnDefaultDisplay_statusBarInAnotherDisplay_visible() = kosmos.runTest { - val latest by collectLastValue(underTest.isHomeStatusBarAllowedByScene) + val underTest = homeStatusBarViewModelFactory(EXTERNAL_DISPLAY) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) kosmos.sceneContainerRepository.snapToScene(Scenes.Gone) kosmos.sceneContainerRepository.showOverlay(Overlays.QuickSettingsShade) runCurrent() + assertThat(latest).isTrue() + } + + @Test + @EnableSceneContainer + fun isHomeStatusBarAllowed_onDefaultDisplayLockscreen_invisible() = + kosmos.runTest { + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) + + kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) + runCurrent() + + assertThat(latest).isFalse() + } + + @Test + @EnableSceneContainer + @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun isHomeStatusBarAllowed_onExternalDispalyWithLocksceren_invisible() = + kosmos.runTest { + val underTest = homeStatusBarViewModelFactory(EXTERNAL_DISPLAY) + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) + + kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) + runCurrent() + + assertThat(latest).isFalse() + } + + @Test + @DisableSceneContainer + fun isHomeStatusBarAllowed_legacy_onDefaultDisplayLockscreen_invisible() = + kosmos.runTest { + val latest by collectLastValue(underTest.isHomeStatusBarAllowed) + + kosmos.fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.GONE, + KeyguardState.LOCKSCREEN, + ) + + runCurrent() + assertThat(latest).isFalse() } @@ -716,7 +816,8 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test - fun canShowOngoingActivityChips_statusBarNotHidden_noSecureCamera_hun_false() = + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun canShowOngoingActivityChips_statusBarNotHidden_noSecureCamera_hunBySystem_noHunFlagOff_false() = kosmos.runTest { val latest by collectLastValue(underTest.canShowOngoingActivityChips) @@ -725,7 +826,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { headsUpNotificationRepository.setNotifications( UnconfinedFakeHeadsUpRowRepository( key = "key", - pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), ) ) @@ -733,6 +834,194 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun canShowOngoingActivityChips_statusBarNotHidden_noSecureCamera_hunByUser_noHunFlagOff_true() = + kosmos.runTest { + val latest by collectLastValue(underTest.canShowOngoingActivityChips) + + transitionKeyguardToGone() + + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + assertThat(latest).isTrue() + } + + @Test + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun canShowOngoingActivityChips_statusBarNotHidden_noSecureCamera_hunBySystem_noHunFlagOn_true() = + kosmos.runTest { + val latest by collectLastValue(underTest.canShowOngoingActivityChips) + + transitionKeyguardToGone() + + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + assertThat(latest).isTrue() + } + + @Test + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun canShowOngoingActivityChips_statusBarNotHidden_noSecureCamera_hunByUser_noHunFlagOn_true() = + kosmos.runTest { + val latest by collectLastValue(underTest.canShowOngoingActivityChips) + + transitionKeyguardToGone() + + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + assertThat(latest).isTrue() + } + + @Test + @EnableChipsModernization + fun ongoingActivityChips_statusBarHidden_noSecureCamera_noHun_notAllowed() = + kosmos.runTest { + val latest by collectLastValue(underTest.ongoingActivityChips) + + // home status bar not allowed + kosmos.sceneContainerRepository.snapToScene(Scenes.Lockscreen) + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false, taskInfo = null) + + assertThat(latest!!.areChipsAllowed).isFalse() + } + + @Test + @EnableChipsModernization + fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_noHun_isAllowed() = + kosmos.runTest { + val latest by collectLastValue(underTest.ongoingActivityChips) + + transitionKeyguardToGone() + + assertThat(latest!!.areChipsAllowed).isTrue() + } + + @Test + @EnableChipsModernization + fun ongoingActivityChips_statusBarNotHidden_secureCamera_noHun_notAllowed() = + kosmos.runTest { + val latest by collectLastValue(underTest.ongoingActivityChips) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + testScope = testScope, + ) + kosmos.keyguardInteractor.onCameraLaunchDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) + + assertThat(latest!!.areChipsAllowed).isFalse() + } + + @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + @EnableChipsModernization + fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_hunBySystem_noHunFlagOff_notAllowed() = + kosmos.runTest { + val latest by collectLastValue(underTest.ongoingActivityChips) + + transitionKeyguardToGone() + + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + assertThat(latest!!.areChipsAllowed).isFalse() + } + + @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + @EnableChipsModernization + fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_hunByUser_noHunFlagOff_isAllowed() = + kosmos.runTest { + val latest by collectLastValue(underTest.ongoingActivityChips) + + transitionKeyguardToGone() + + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + assertThat(latest!!.areChipsAllowed).isTrue() + } + + @Test + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + @EnableChipsModernization + fun ongoingActivityChips_tatusBarNotHidden_noSecureCamera_hunBySystem_noHunFlagOn_isAllowed() = + kosmos.runTest { + val latest by collectLastValue(underTest.ongoingActivityChips) + + transitionKeyguardToGone() + + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + assertThat(latest!!.areChipsAllowed).isTrue() + } + + @Test + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + @EnableChipsModernization + fun ongoingActivityChips_statusBarNotHidden_noSecureCamera_hunByUser_noHunFlagOn_isAllowed() = + kosmos.runTest { + val latest by collectLastValue(underTest.ongoingActivityChips) + + transitionKeyguardToGone() + + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + assertThat(latest!!.areChipsAllowed).isTrue() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + @EnableChipsModernization + fun ongoingActivityChips_followsChipsViewModel() = + kosmos.runTest { + val latest by collectLastValue(underTest.ongoingActivityChips) + transitionKeyguardToGone() + + screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + + assertIsScreenRecordChip(latest!!.chips.active[0]) + + addOngoingCallState(key = "call") + + assertIsScreenRecordChip(latest!!.chips.active[0]) + assertIsCallChip(latest!!.chips.active[1], "call", context) + } + + @Test fun isClockVisible_allowedByDisableFlags_visible() = kosmos.runTest { val latest by collectLastValue(underTest.isClockVisible) @@ -892,7 +1181,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test @EnableChipsModernization - fun isNotificationIconContainerVisible_anyChipShowing_ChipsModernizationOn() = + fun isNotificationIconContainerVisible_anyChipShowing_chipsModernizationOn() = kosmos.runTest { val latest by collectLastValue(underTest.isNotificationIconContainerVisible) transitionKeyguardToGone() @@ -909,7 +1198,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test @DisableFlags(StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME) @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun isNotificationIconContainerVisible_anyChipShowing_PromotedNotifsOn() = + fun isNotificationIconContainerVisible_anyChipShowing_promotedNotifsOn() = kosmos.runTest { val latest by collectLastValue(underTest.isNotificationIconContainerVisible) transitionKeyguardToGone() @@ -929,7 +1218,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME, ) - fun isNotificationIconContainerVisible_anyChipShowing_ChipsModernizationAndPromotedNotifsOff() = + fun isNotificationIconContainerVisible_anyChipShowing_chipsModernizationAndPromotedNotifsOff() = kosmos.runTest { val latest by collectLastValue(underTest.isNotificationIconContainerVisible) transitionKeyguardToGone() @@ -943,6 +1232,86 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { assertThat(latest!!.visibility).isEqualTo(View.VISIBLE) } + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isNotificationIconContainerVisible_hasChipButAlsoHun_hunBySystem_noHunFlagOff_visible() = + kosmos.runTest { + val latest by collectLastValue(underTest.isNotificationIconContainerVisible) + transitionKeyguardToGone() + + // Chip + kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + + // HUN, PinnedBySystem + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + assertThat(latest!!.visibility).isEqualTo(View.VISIBLE) + } + + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isNotificationIconContainerVisible_hasChipButAlsoHun_hunByUser_noHunFlagOff_gone() = + kosmos.runTest { + val latest by collectLastValue(underTest.isNotificationIconContainerVisible) + transitionKeyguardToGone() + + // Chip + kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + + // HUN, PinnedByUser + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + assertThat(latest!!.visibility).isEqualTo(View.GONE) + } + + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isNotificationIconContainerVisible_hasChipButAlsoHun_hunBySystem_noHunFlagOn_gone() = + kosmos.runTest { + val latest by collectLastValue(underTest.isNotificationIconContainerVisible) + transitionKeyguardToGone() + + // Chip + kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + + // HUN, PinnedBySystem + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + assertThat(latest!!.visibility).isEqualTo(View.GONE) + } + + @EnableFlags(StatusBarNoHunBehavior.FLAG_NAME) + fun isNotificationIconContainerVisible_hasChipButAlsoHun_hunByUser_noHunFlagOn_gone() = + kosmos.runTest { + val latest by collectLastValue(underTest.isNotificationIconContainerVisible) + transitionKeyguardToGone() + + // Chip + kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording + + // HUN, PinnedByUser + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + assertThat(latest!!.visibility).isEqualTo(View.GONE) + } + @Test fun isSystemInfoVisible_allowedByDisableFlags_visible() = kosmos.runTest { @@ -1289,4 +1658,8 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { testScope = testScope, ) } + + private companion object { + const val EXTERNAL_DISPLAY = 1 + } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt index 4c1f6450dd78..b52db83d513c 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt @@ -42,9 +42,13 @@ interface ClockController { isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float, - onBoundsChanged: (RectF) -> Unit, + clockListener: ClockEventListener?, ) /** Optional method for dumping debug information */ fun dump(pw: PrintWriter) } + +interface ClockEventListener { + fun onBoundsChanged(bounds: RectF) +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/dagger/qualifiers/DisplaySpecific.kt b/packages/SystemUI/pods/com/android/systemui/dagger/qualifiers/DisplaySpecific.kt index 3e39ae9a3fe5..0c46e0784a31 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/dagger/qualifiers/DisplaySpecific.kt +++ b/packages/SystemUI/pods/com/android/systemui/dagger/qualifiers/DisplaySpecific.kt @@ -18,4 +18,7 @@ package com.android.systemui.dagger.qualifiers import javax.inject.Qualifier /** Annotates a class that is display specific. */ -@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class DisplaySpecific +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +public annotation class DisplaySpecific diff --git a/packages/SystemUI/shared/src/com/android/systemui/dagger/qualifiers/Main.java b/packages/SystemUI/pods/com/android/systemui/dagger/qualifiers/Main.java index 7b097740ff11..7b097740ff11 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/dagger/qualifiers/Main.java +++ b/packages/SystemUI/pods/com/android/systemui/dagger/qualifiers/Main.java diff --git a/packages/SystemUI/shared/src/com/android/systemui/dagger/qualifiers/UiBackground.java b/packages/SystemUI/pods/com/android/systemui/dagger/qualifiers/UiBackground.java index 3d37468c322a..3d37468c322a 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/dagger/qualifiers/UiBackground.java +++ b/packages/SystemUI/pods/com/android/systemui/dagger/qualifiers/UiBackground.java diff --git a/packages/SystemUI/res-keyguard/drawable/qs_media_recommendation_bg_gradient.xml b/packages/SystemUI/res-keyguard/drawable/qs_media_recommendation_bg_gradient.xml deleted file mode 100644 index 495fbb893eac..000000000000 --- a/packages/SystemUI/res-keyguard/drawable/qs_media_recommendation_bg_gradient.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2021 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<shape - xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners android:radius="24dp"/> - <gradient - android:angle="0" - android:startColor="#00000000" - android:endColor="#ff000000" - android:type="linear" /> -</shape> diff --git a/packages/SystemUI/res/drawable/qs_media_rec_scrim.xml b/packages/SystemUI/res/drawable/qs_media_rec_scrim.xml deleted file mode 100644 index de0a6201cb09..000000000000 --- a/packages/SystemUI/res/drawable/qs_media_rec_scrim.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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 - --> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <!-- gradient from 25% in the center to 100% at edges --> - <gradient - android:type="radial" - android:gradientRadius="40%p" - android:startColor="#AE000000" - android:endColor="#00000000" /> -</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/magic_action_button.xml b/packages/SystemUI/res/layout/magic_action_button.xml index 381d6b17dec5..63fc1e485635 100644 --- a/packages/SystemUI/res/layout/magic_action_button.xml +++ b/packages/SystemUI/res/layout/magic_action_button.xml @@ -6,7 +6,7 @@ android:background="@drawable/magic_action_button_background" android:drawablePadding="@dimen/magic_action_button_drawable_padding" android:ellipsize="none" - android:fontFamily="google-sans-flex" + android:fontFamily="google-sans-flex-medium" android:gravity="center" android:minWidth="0dp" android:paddingHorizontal="@dimen/magic_action_button_padding_horizontal" diff --git a/packages/SystemUI/res/layout/media_recommendation_view.xml b/packages/SystemUI/res/layout/media_recommendation_view.xml deleted file mode 100644 index e63aa211f9f1..000000000000 --- a/packages/SystemUI/res/layout/media_recommendation_view.xml +++ /dev/null @@ -1,90 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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 - --> -<!-- Layout for media recommendation item inside QSPanel carousel --> -<merge xmlns:android="http://schemas.android.com/apk/res/android"> - <!-- Album cover --> - <ImageView - android:id="@+id/media_cover" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:translationZ="0dp" - android:scaleType="matrix" - android:adjustViewBounds="true" - android:clipToOutline="true" - android:layerType="hardware" - android:background="@drawable/bg_smartspace_media_item"/> - - <!-- App icon --> - <com.android.internal.widget.CachingIconView - android:id="@+id/media_rec_app_icon" - android:layout_width="@dimen/qs_media_rec_album_icon_size" - android:layout_height="@dimen/qs_media_rec_album_icon_size" - android:minWidth="@dimen/qs_media_rec_album_icon_size" - android:minHeight="@dimen/qs_media_rec_album_icon_size" - android:layout_marginStart="@dimen/qs_media_info_spacing" - android:layout_marginTop="@dimen/qs_media_info_spacing"/> - - <!-- Artist name --> - <TextView - android:id="@+id/media_title" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/qs_media_info_spacing" - android:layout_marginEnd="@dimen/qs_media_info_spacing" - android:layout_marginBottom="@dimen/qs_media_rec_album_title_bottom_margin" - android:fontFamily="@*android:string/config_headlineFontFamilyMedium" - android:singleLine="true" - android:textSize="12sp" - android:gravity="top" - android:layout_gravity="bottom" - android:importantForAccessibility="no"/> - - <!-- Album name --> - <TextView - android:id="@+id/media_subtitle" - android:layout_width="match_parent" - android:layout_height="@dimen/qs_media_rec_album_subtitle_height" - android:layout_marginEnd="@dimen/qs_media_info_spacing" - android:layout_marginStart="@dimen/qs_media_info_spacing" - android:layout_marginBottom="@dimen/qs_media_info_spacing" - android:fontFamily="@*android:string/config_headlineFontFamily" - android:singleLine="true" - android:textSize="11sp" - android:gravity="center_vertical" - android:layout_gravity="bottom" - android:importantForAccessibility="no"/> - - <!-- Seek Bar --> - <SeekBar - android:id="@+id/media_progress_bar" - android:layout_width="match_parent" - android:layout_height="12dp" - android:layout_gravity="bottom" - android:maxHeight="@dimen/qs_media_enabled_seekbar_height" - android:thumb="@android:color/transparent" - android:splitTrack="false" - android:clickable="false" - android:progressTint="?android:attr/textColorPrimary" - android:progressBackgroundTint="?android:attr/textColorTertiary" - android:paddingTop="5dp" - android:paddingBottom="5dp" - android:paddingStart="0dp" - android:paddingEnd="0dp" - android:layout_marginEnd="@dimen/qs_media_info_spacing" - android:layout_marginStart="@dimen/qs_media_info_spacing" - android:layout_marginBottom="@dimen/qs_media_info_spacing"/> -</merge>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/media_recommendations.xml b/packages/SystemUI/res/layout/media_recommendations.xml deleted file mode 100644 index 65fc19c5b2a4..000000000000 --- a/packages/SystemUI/res/layout/media_recommendations.xml +++ /dev/null @@ -1,75 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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 - --> - -<!-- Layout for media recommendations inside QSPanel carousel --> -<com.android.systemui.util.animation.TransitionLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/media_recommendations_updated" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:clipChildren="false" - android:clipToPadding="false" - android:forceHasOverlappingRendering="false" - android:background="@drawable/qs_media_background" - android:theme="@style/MediaPlayer"> - - <!-- This view just ensures the full media player is a certain height. --> - <View - android:id="@+id/sizing_view" - android:layout_width="match_parent" - android:layout_height="@dimen/qs_media_session_height_expanded" /> - - <TextView - android:id="@+id/media_rec_title" - style="@style/MediaPlayer.Recommendation.Header" - android:text="@string/controls_media_smartspace_rec_header"/> - - <FrameLayout - android:id="@+id/media_cover1_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - > - - <include - layout="@layout/media_recommendation_view"/> - - </FrameLayout> - - - <FrameLayout - android:id="@+id/media_cover2_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - > - - <include - layout="@layout/media_recommendation_view"/> - - </FrameLayout> - - <FrameLayout - android:id="@+id/media_cover3_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - > - - <include - layout="@layout/media_recommendation_view"/> - - </FrameLayout> - - <include - layout="@layout/media_long_press_menu" /> - -</com.android.systemui.util.animation.TransitionLayout> diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index f41eaec8e18b..a1fa54cf592d 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -40,7 +40,7 @@ android:layout_marginBottom="@dimen/volume_dialog_components_spacing" android:clipChildren="false" app:layout_constraintBottom_toTopOf="@id/volume_dialog_main_slider_container" - app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" + app:layout_constraintEnd_toEndOf="@id/volume_dialog_background" app:layout_constraintHeight_default="spread" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -70,9 +70,9 @@ android:layout_marginTop="@dimen/volume_dialog_components_spacing" android:clipChildren="false" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" + app:layout_constraintEnd_toEndOf="@id/volume_dialog_background" app:layout_constraintHeight_default="wrap" - app:layout_constraintStart_toStartOf="@id/volume_dialog_main_slider_container" + app:layout_constraintStart_toStartOf="@id/volume_dialog_background" app:layout_constraintTop_toBottomOf="@id/volume_dialog_main_slider_container" app:layout_constraintVertical_bias="0" app:layout_constraintWidth_default="wrap"> diff --git a/packages/SystemUI/res/layout/volume_dialog_top_section.xml b/packages/SystemUI/res/layout/volume_dialog_top_section.xml index 29f52480bfe0..b7455471d9a6 100644 --- a/packages/SystemUI/res/layout/volume_dialog_top_section.xml +++ b/packages/SystemUI/res/layout/volume_dialog_top_section.xml @@ -22,7 +22,7 @@ android:clipChildren="false" android:clipToPadding="false" android:gravity="center" - android:paddingEnd="@dimen/volume_dialog_ringer_drawer_diff_end_margin" + android:paddingEnd="@dimen/volume_dialog_buttons_margin" app:layoutDescription="@xml/volume_dialog_ringer_drawer_motion_scene"> <View diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml index 41bb37efa623..f4f0424ade98 100644 --- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml @@ -30,11 +30,6 @@ not appear immediately after user swipes to the side --> <dimen name="qs_tiles_page_horizontal_margin">20dp</dimen> - <!-- Size of Smartspace media recommendations cards in the QSPanel carousel --> - <dimen name="qs_media_rec_icon_top_margin">16dp</dimen> - <dimen name="qs_media_rec_album_size">112dp</dimen> - <dimen name="qs_media_rec_album_side_margin">16dp</dimen> - <dimen name="controls_panel_corner_radius">40dp</dimen> <dimen name="lockscreen_shade_max_over_scroll_amount">42dp</dimen> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 4995858f95a4..78e719f6289a 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -98,10 +98,6 @@ TODO (b/293252410) - change this comment/resource when flag is enabled --> <integer name="small_land_lockscreen_quick_settings_max_rows">2</integer> - <!-- If the dp width of the available space is <= this value, potentially adjust the number - of media recommendation items--> - <integer name="default_qs_media_rec_width_dp">380</integer> - <!-- The number of columns that the top level tiles span in the QuickSettings --> <!-- The default tiles to display in QuickSettings --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 7c370d3bc064..8342a9cc244b 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1342,19 +1342,6 @@ <dimen name="qs_media_session_collapsed_legacy_guideline">144dp</dimen> <dimen name="qs_media_session_collapsed_guideline">168dp</dimen> - <!-- Size of Smartspace media recommendations cards in the QSPanel carousel --> - <dimen name="qs_media_rec_default_width">380dp</dimen> - <dimen name="qs_media_rec_icon_top_margin">16dp</dimen> - <dimen name="qs_media_rec_album_icon_size">16dp</dimen> - <dimen name="qs_media_rec_album_size">88dp</dimen> - <dimen name="qs_media_rec_album_width">110dp</dimen> - <dimen name="qs_media_rec_album_height_expanded">108dp</dimen> - <dimen name="qs_media_rec_album_height_collapsed">77dp</dimen> - <dimen name="qs_media_rec_album_side_margin">16dp</dimen> - <dimen name="qs_media_rec_album_bottom_margin">8dp</dimen> - <dimen name="qs_media_rec_album_title_bottom_margin">22dp</dimen> - <dimen name="qs_media_rec_album_subtitle_height">12dp</dimen> - <!-- Chipbar --> <!-- (Used for media tap-to-transfer chip for sender device and active unlock) --> <dimen name="chipbar_outer_padding">16dp</dimen> @@ -2202,11 +2189,6 @@ <dimen name="volume_dialog_background_square_corner_radius">12dp</dimen> <dimen name="volume_dialog_ringer_drawer_margin">@dimen/volume_dialog_buttons_margin</dimen> - <!-- - (volume_dialog_slider_width - volume_dialog_button_size) / 2 - This centers ringer drawer against the volume slider - --> - <dimen name="volume_dialog_ringer_drawer_diff_end_margin">6dp</dimen> <dimen name="volume_dialog_ringer_drawer_button_size">@dimen/volume_dialog_button_size</dimen> <dimen name="volume_dialog_ringer_drawer_button_icon_radius">10dp</dimen> <dimen name="volume_dialog_ringer_selected_button_background_radius">20dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 43ea2c3f7633..c06c17a0844f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1042,6 +1042,8 @@ <string name="hearing_devices_tools_label">Tools</string> <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40] [BACKUP_MESSAGE_ID=8916875614623730005]--> <string name="quick_settings_hearing_devices_live_caption_title">Live Caption</string> + <!-- QuickSettings: Label for button to go to hearing devices settings page [CHAR_LIMIT=20] --> + <string name="hearing_devices_settings_button">Settings</string> <!-- QuickSettings: Notes tile. The label of a quick settings tile for launching the default notes taking app. [CHAR LIMIT=NONE] --> <string name="quick_settings_notes_label">Note</string> @@ -2599,6 +2601,9 @@ <!-- Accessibility description indicating the currently selected tile's position. Only used for tiles that are currently in use [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_position">Position <xliff:g id="position" example="5">%1$d</xliff:g></string> + <!-- Accessibility description indicating the currently selected tile is already added [CHAR LIMIT=NONE] --> + <string name="accessibility_qs_edit_tile_already_added">Tile already added</string> + <!-- Accessibility announcement after a tile has been added [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_tile_added">Tile added</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 4431ddadc8de..7895ff7b90f6 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -895,57 +895,6 @@ <item name="android:textColor">@android:color/system_on_primary_dark</item> </style> - <style name="MediaPlayer.Recommendation"/> - - <style name="MediaPlayer.Recommendation.Header"> - <item name="android:layout_width">wrap_content</item> - <item name="android:layout_height">wrap_content</item> - <item name="android:layout_marginTop">@dimen/qs_media_padding</item> - <item name="android:layout_marginStart">@dimen/qs_media_padding</item> - <item name="android:fontFamily">=@*android:string/config_headlineFontFamilyMedium</item> - <item name="android:singleLine">true</item> - <item name="android:textSize">14sp</item> - <item name="android:textColor">?android:attr/textColorPrimary</item> - </style> - - <style name="MediaPlayer.Recommendation.AlbumContainer"> - <item name="android:layout_width">@dimen/qs_media_rec_album_size</item> - <item name="android:layout_height">@dimen/qs_media_rec_album_size</item> - <item name="android:background">@drawable/qs_media_light_source</item> - <item name="android:layout_marginTop">@dimen/qs_media_padding</item> - <item name="android:layout_marginBottom">@dimen/qs_media_rec_album_bottom_margin</item> - </style> - - <style name="MediaPlayer.Recommendation.AlbumContainer.Updated"> - <item name="android:layout_width">@dimen/qs_media_rec_album_width</item> - <item name="android:minWidth">@dimen/qs_media_rec_album_width</item> - <item name="android:minHeight">@dimen/qs_media_rec_album_height_collapsed</item> - <item name="android:background">@drawable/qs_media_light_source</item> - <item name="android:layout_marginTop">@dimen/qs_media_info_spacing</item> - </style> - - <style name="MediaPlayer.Recommendation.Album"> - <item name="android:backgroundTint">@color/media_player_album_bg</item> - </style> - - <style name="MediaPlayer.Recommendation.Text"> - <item name="android:layout_width">@dimen/qs_media_rec_album_size</item> - <item name="android:layout_height">wrap_content</item> - <item name="android:maxLines">1</item> - <item name="android:ellipsize">end</item> - <item name="android:textSize">14sp</item> - <item name="android:gravity">start</item> - </style> - - <style name="MediaPlayer.Recommendation.Text.Title"> - <item name="android:textColor">?android:attr/textColorPrimary</item> - </style> - - <style name="MediaPlayer.Recommendation.Text.Subtitle"> - <item name="android:textColor">?android:attr/textColorSecondary</item> - </style> - - <!-- Used to style charging animation AVD animation --> <style name="ChargingAnim" /> diff --git a/packages/SystemUI/res/xml/media_recommendations_collapsed.xml b/packages/SystemUI/res/xml/media_recommendations_collapsed.xml deleted file mode 100644 index d3be3c7de5ad..000000000000 --- a/packages/SystemUI/res/xml/media_recommendations_collapsed.xml +++ /dev/null @@ -1,64 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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 - --> -<ConstraintSet - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - > - - <Constraint - android:id="@+id/sizing_view" - android:layout_width="match_parent" - android:layout_height="@dimen/qs_media_session_height_collapsed" - /> - - <Constraint - android:id="@+id/media_rec_title" - style="@style/MediaPlayer.Recommendation.Header" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"/> - - <Constraint - android:id="@+id/media_cover1_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - android:layout_height="@dimen/qs_media_rec_album_height_collapsed" - android:layout_marginEnd="@dimen/qs_media_info_spacing" - android:layout_marginStart="@dimen/qs_media_padding" - app:layout_constraintTop_toBottomOf="@+id/media_rec_title" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/media_cover2_container"/> - - - <Constraint - android:id="@+id/media_cover2_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - android:layout_height="@dimen/qs_media_rec_album_height_collapsed" - android:layout_marginEnd="@dimen/qs_media_info_spacing" - app:layout_constraintTop_toBottomOf="@+id/media_rec_title" - app:layout_constraintStart_toEndOf="@id/media_cover1_container" - app:layout_constraintEnd_toStartOf="@id/media_cover3_container"/> - - <Constraint - android:id="@+id/media_cover3_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - android:layout_height="@dimen/qs_media_rec_album_height_collapsed" - android:layout_marginEnd="@dimen/qs_media_padding" - app:layout_constraintTop_toBottomOf="@+id/media_rec_title" - app:layout_constraintStart_toEndOf="@id/media_cover2_container" - app:layout_constraintEnd_toEndOf="parent"/> - - -</ConstraintSet> diff --git a/packages/SystemUI/res/xml/media_recommendations_expanded.xml b/packages/SystemUI/res/xml/media_recommendations_expanded.xml deleted file mode 100644 index 88c70552e9e8..000000000000 --- a/packages/SystemUI/res/xml/media_recommendations_expanded.xml +++ /dev/null @@ -1,71 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ 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 - --> -<ConstraintSet - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - > - - <Constraint - android:id="@+id/sizing_view" - android:layout_width="match_parent" - android:layout_height="@dimen/qs_media_session_height_expanded" - /> - - <Constraint - android:id="@+id/media_rec_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/qs_media_padding" - android:layout_marginStart="@dimen/qs_media_padding" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - android:fontFamily="@*android:string/config_headlineFontFamilyMedium" - android:singleLine="true" - android:textSize="14sp" - android:textColor="@color/notification_primary_text_color"/> - - <Constraint - android:id="@+id/media_cover1_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - android:layout_height="@dimen/qs_media_rec_album_height_expanded" - android:layout_marginEnd="@dimen/qs_media_info_spacing" - android:layout_marginStart="@dimen/qs_media_padding" - app:layout_constraintTop_toBottomOf="@+id/media_rec_title" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/media_cover2_container"/> - - - <Constraint - android:id="@+id/media_cover2_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - android:layout_height="@dimen/qs_media_rec_album_height_expanded" - android:layout_marginEnd="@dimen/qs_media_info_spacing" - app:layout_constraintTop_toBottomOf="@+id/media_rec_title" - app:layout_constraintStart_toEndOf="@id/media_cover1_container" - app:layout_constraintEnd_toStartOf="@id/media_cover3_container"/> - - <Constraint - android:id="@+id/media_cover3_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer.Updated" - android:layout_height="@dimen/qs_media_rec_album_height_expanded" - android:layout_marginEnd="@dimen/qs_media_padding" - app:layout_constraintTop_toBottomOf="@+id/media_rec_title" - app:layout_constraintStart_toEndOf="@id/media_cover2_container" - app:layout_constraintEnd_toEndOf="parent"/> - - -</ConstraintSet> diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index ae3a76e2d2ca..8d7cab41ea20 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -51,6 +51,7 @@ android_library { ":wm_shell-shared-aidls", ], static_libs: [ + "com.android.systemui.dagger-api", "BiometricsSharedLib", "PlatformAnimationLib", "PluginCoreLib", diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java index dcbacec5b630..c880f057f44a 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java @@ -203,7 +203,9 @@ public class CarrierTextManager { CarrierTextManagerLogger logger) { mContext = context; - mIsEmergencyCallCapable = telephonyManager.isVoiceCapable(); + boolean hasTelephony = mContext.getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + mIsEmergencyCallCapable = telephonyManager.isVoiceCapable() && hasTelephony; mShowAirplaneMode = showAirplaneMode; mShowMissingSim = showMissingSim; @@ -221,9 +223,7 @@ public class CarrierTextManager { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLogger = logger; mBgExecutor.execute(() -> { - boolean supported = mContext.getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_TELEPHONY); - if (supported && mNetworkSupported.compareAndSet(false, supported)) { + if (hasTelephony && mNetworkSupported.compareAndSet(false, hasTelephony)) { // This will set/remove the listeners appropriately. Note that it will never double // add the listeners. handleSetListening(mCarrierTextCallback); diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index e827b2d54a19..1549b699eee6 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -56,6 +56,7 @@ import com.android.systemui.log.core.Logger import com.android.systemui.modes.shared.ModesUi import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.ClockEventListener import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockMessageBuffers import com.android.systemui.plugins.clocks.ClockTickRate @@ -148,7 +149,7 @@ constructor( val clockStr = clock.toString() loggers.forEach { it.d({ "New Clock: $str1" }) { str1 = clockStr } } - clock.initialize(isDarkTheme(), dozeAmount.value, 0f, { onClockBoundsChanged.value = it }) + clock.initialize(isDarkTheme(), dozeAmount.value, 0f, clockListener) if (!regionSamplingEnabled) { updateColors() @@ -312,6 +313,13 @@ constructor( private var zenData: ZenData? = null private var alarmData: AlarmData? = null + private val clockListener = + object : ClockEventListener { + override fun onBoundsChanged(bounds: RectF) { + onClockBoundsChanged.value = bounds + } + } + private val configListener = object : ConfigurationController.ConfigurationListener { override fun onThemeChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index b730c931be8b..08559f2eca8d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -19,6 +19,8 @@ package com.android.systemui.accessibility.hearingaid; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; + import static java.util.Collections.emptyList; import android.bluetooth.BluetoothHapClient; @@ -263,6 +265,20 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, dialog.setTitle(R.string.quick_settings_hearing_devices_dialog_title); dialog.setView(LayoutInflater.from(dialog.getContext()).inflate( R.layout.hearing_devices_tile_dialog, null)); + dialog.setNegativeButton( + R.string.hearing_devices_settings_button, + (dialogInterface, which) -> { + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_SETTINGS_CLICK, + mLaunchSourceId); + final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS) + .putExtra(Intent.EXTRA_COMPONENT_NAME, + ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString()); + mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, + mDialogTransitionAnimator.createActivityTransitionController( + dialog)); + }, + /* dismissOnClick = */ true + ); dialog.setPositiveButton( R.string.quick_settings_done, /* onClick = */ null, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt index fe1d5040c6f5..4a695d638713 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt @@ -39,7 +39,9 @@ enum class HearingDevicesUiEvent(private val id: Int) : UiEventLogger.UiEventEnu @UiEvent(doc = "Expand the ambient volume controls") HEARING_DEVICES_AMBIENT_EXPAND_CONTROLS(2153), @UiEvent(doc = "Collapse the ambient volume controls") - HEARING_DEVICES_AMBIENT_COLLAPSE_CONTROLS(2154); + HEARING_DEVICES_AMBIENT_COLLAPSE_CONTROLS(2154), + @UiEvent(doc = "Click on the device settings to enter hearing devices page") + HEARING_DEVICES_SETTINGS_CLICK(2172); override fun getId(): Int = this.id } diff --git a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt index 0b578c65e915..113df2026e81 100644 --- a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt @@ -122,6 +122,10 @@ constructor( return false } + fun isBackCallbackRegistered(): Boolean { + return isCallbackRegistered + } + private fun registerBackCallback() { if (isCallbackRegistered) { return diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 88694ae6db51..dfe8eb28b2a6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -179,7 +179,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final PowerInteractor mPowerInteractor; @NonNull private final CoroutineScope mScope; @NonNull private final InputManager mInputManager; - @NonNull private final UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate; @NonNull private final SelectedUserInteractor mSelectedUserInteractor; private final boolean mIgnoreRefreshRate; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; @@ -292,7 +291,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { mActivityTransitionAnimator, mPrimaryBouncerInteractor, mAlternateBouncerInteractor, - mUdfpsKeyguardAccessibilityDelegate, mKeyguardTransitionInteractor, mSelectedUserInteractor, mDeviceEntryUdfpsTouchOverlayViewModel, @@ -691,7 +689,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull AlternateBouncerInteractor alternateBouncerInteractor, @NonNull InputManager inputManager, @NonNull DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor, - @NonNull UdfpsKeyguardAccessibilityDelegate udfpsKeyguardAccessibilityDelegate, @NonNull SelectedUserInteractor selectedUserInteractor, @NonNull KeyguardTransitionInteractor keyguardTransitionInteractor, Lazy<DeviceEntryUdfpsTouchOverlayViewModel> deviceEntryUdfpsTouchOverlayViewModel, @@ -742,7 +739,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { mPowerInteractor = powerInteractor; mScope = scope; mInputManager = inputManager; - mUdfpsKeyguardAccessibilityDelegate = udfpsKeyguardAccessibilityDelegate; mSelectedUserInteractor = selectedUserInteractor; mKeyguardTransitionInteractor = keyguardTransitionInteractor; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 702f23718ee8..bdf58275effa 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -107,7 +107,6 @@ constructor( private val primaryBouncerInteractor: PrimaryBouncerInteractor, private val alternateBouncerInteractor: AlternateBouncerInteractor, private val isDebuggable: Boolean = Build.IS_DEBUGGABLE, - private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate, private val transitionInteractor: KeyguardTransitionInteractor, private val selectedUserInteractor: SelectedUserInteractor, private val deviceEntryUdfpsTouchOverlayViewModel: Lazy<DeviceEntryUdfpsTouchOverlayViewModel>, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt deleted file mode 100644 index 99da660d1fda..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardAccessibilityDelegate.kt +++ /dev/null @@ -1,54 +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.systemui.biometrics - -import android.content.res.Resources -import android.os.Bundle -import android.view.View -import android.view.accessibility.AccessibilityNodeInfo -import com.android.systemui.res.R -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager -import javax.inject.Inject - -@SysUISingleton -class UdfpsKeyguardAccessibilityDelegate -@Inject -constructor( - @Main private val resources: Resources, - private val keyguardViewManager: StatusBarKeyguardViewManager, -) : View.AccessibilityDelegate() { - override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) { - super.onInitializeAccessibilityNodeInfo(host, info) - val clickAction = - AccessibilityNodeInfo.AccessibilityAction( - AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, - resources.getString(R.string.accessibility_bouncer) - ) - info.addAction(clickAction) - } - - override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { - // when an a11y service is enabled, double tapping on the fingerprint sensor should - // show the primary bouncer - return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) { - keyguardViewManager.showPrimaryBouncer(/* scrimmed */ true) - true - } else super.performAccessibilityAction(host, action, args) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 79748a255ed0..52204b84346d 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -165,20 +165,15 @@ fun BrightnessSlider( val activeIconColor = colors.activeTickColor val inactiveIconColor = colors.inactiveTickColor - val trackIcon: DrawScope.(Offset, Color, Float) -> Unit = - remember(painter) { - { offset, color, alpha -> - translate(offset.x + IconPadding.toPx(), offset.y) { - with(painter) { - draw( - IconSize.toSize(), - colorFilter = ColorFilter.tint(color), - alpha = alpha, - ) - } + val trackIcon: DrawScope.(Offset, Color, Float) -> Unit = remember { + { offset, color, alpha -> + translate(offset.x + IconPadding.toPx(), offset.y) { + with(painter) { + draw(IconSize.toSize(), colorFilter = ColorFilter.tint(color), alpha = alpha) } } } + } Slider( value = animatedValue, diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt new file mode 100644 index 000000000000..df6c1b18e3e9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ActionIntentCreator.kt @@ -0,0 +1,99 @@ +/* + * 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.clipboardoverlay + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.text.TextUtils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.res.R +import javax.inject.Inject + +@SysUISingleton +class ActionIntentCreator @Inject constructor() : IntentCreator { + override fun getTextEditorIntent(context: Context?) = + Intent(context, EditTextActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + + override fun getShareIntent(clipData: ClipData, context: Context?): Intent { + val shareIntent = Intent(Intent.ACTION_SEND) + + // From the ACTION_SEND docs: + // "If using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it should be the + // MIME type of the data in EXTRA_STREAM" + val uri = clipData.getItemAt(0).uri + shareIntent.apply { + if (uri != null) { + // We don't use setData here because some apps interpret this as "to:". + setType(clipData.description.getMimeType(0)) + // Include URI in ClipData also, so that grantPermission picks it up. + setClipData( + ClipData( + ClipDescription("content", arrayOf(clipData.description.getMimeType(0))), + ClipData.Item(uri), + ) + ) + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } else { + putExtra(Intent.EXTRA_TEXT, clipData.getItemAt(0).coerceToText(context).toString()) + setType("text/plain") + } + } + + return Intent.createChooser(shareIntent, null) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + override fun getImageEditIntent(uri: Uri?, context: Context): Intent { + val editorPackage = context.getString(R.string.config_screenshotEditor) + return Intent(Intent.ACTION_EDIT).apply { + if (!TextUtils.isEmpty(editorPackage)) { + setComponent(ComponentName.unflattenFromString(editorPackage)) + } + setDataAndType(uri, "image/*") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + putExtra(EXTRA_EDIT_SOURCE, EDIT_SOURCE_CLIPBOARD) + } + } + + override fun getRemoteCopyIntent(clipData: ClipData?, context: Context): Intent { + val remoteCopyPackage = context.getString(R.string.config_remoteCopyPackage) + return Intent(REMOTE_COPY_ACTION).apply { + if (!TextUtils.isEmpty(remoteCopyPackage)) { + setComponent(ComponentName.unflattenFromString(remoteCopyPackage)) + } + + setClipData(clipData) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + } + + companion object { + private const val EXTRA_EDIT_SOURCE: String = "edit_source" + private const val EDIT_SOURCE_CLIPBOARD: String = "clipboard" + private const val REMOTE_COPY_ACTION: String = "android.intent.action.REMOTE_COPY" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index ac747845267c..314b6e7f5a28 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -65,7 +65,6 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; import com.android.systemui.screenshot.TimeoutHandler; @@ -94,13 +93,13 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv private final ClipboardOverlayWindow mWindow; private final TimeoutHandler mTimeoutHandler; private final ClipboardOverlayUtils mClipboardUtils; - private final FeatureFlags mFeatureFlags; private final Executor mBgExecutor; private final ClipboardImageLoader mClipboardImageLoader; private final ClipboardTransitionExecutor mTransitionExecutor; private final ClipboardOverlayView mView; private final ClipboardIndicationProvider mClipboardIndicationProvider; + private final IntentCreator mIntentCreator; private Runnable mOnSessionCompleteListener; private Runnable mOnRemoteCopyTapped; @@ -189,13 +188,13 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv BroadcastDispatcher broadcastDispatcher, BroadcastSender broadcastSender, TimeoutHandler timeoutHandler, - FeatureFlags featureFlags, ClipboardOverlayUtils clipboardUtils, @Background Executor bgExecutor, ClipboardImageLoader clipboardImageLoader, ClipboardTransitionExecutor transitionExecutor, ClipboardIndicationProvider clipboardIndicationProvider, - UiEventLogger uiEventLogger) { + UiEventLogger uiEventLogger, + IntentCreator intentCreator) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; mClipboardImageLoader = clipboardImageLoader; @@ -203,6 +202,7 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv mClipboardIndicationProvider = clipboardIndicationProvider; mClipboardLogger = new ClipboardLogger(uiEventLogger); + mIntentCreator = intentCreator; mView = clipboardOverlayView; mWindow = clipboardOverlayWindow; @@ -211,7 +211,6 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv hideImmediate(); }); - mFeatureFlags = featureFlags; mTimeoutHandler = timeoutHandler; mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS); @@ -508,7 +507,8 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv } private void maybeShowRemoteCopy(ClipData clipData) { - Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext); + Intent remoteCopyIntent = mIntentCreator.getRemoteCopyIntent(clipData, mContext); + // Only show remote copy if it's available. PackageManager packageManager = mContext.getPackageManager(); if (packageManager.resolveActivity( @@ -558,19 +558,19 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv private void editImage(Uri uri) { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); - mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext)); + mContext.startActivity(mIntentCreator.getImageEditIntent(uri, mContext)); animateOut(); } private void editText() { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED); - mContext.startActivity(IntentCreator.getTextEditorIntent(mContext)); + mContext.startActivity(mIntentCreator.getTextEditorIntent(mContext)); animateOut(); } private void shareContent(ClipData clip) { mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED); - mContext.startActivity(IntentCreator.getShareIntent(clip, mContext)); + mContext.startActivity(mIntentCreator.getShareIntent(clip, mContext)); animateOut(); } @@ -717,22 +717,22 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv public void onRemoteCopyButtonTapped() { if (clipboardSharedTransitions()) { finish(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED, - IntentCreator.getRemoteCopyIntent(mClipboardModel.getClipData(), mContext)); + mIntentCreator.getRemoteCopyIntent(mClipboardModel.getClipData(), mContext)); } } @Override public void onShareButtonTapped() { if (clipboardSharedTransitions()) { + Intent shareIntent = + mIntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext); switch (mClipboardModel.getType()) { case TEXT: case URI: - finish(CLIPBOARD_OVERLAY_SHARE_TAPPED, - IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext)); + finish(CLIPBOARD_OVERLAY_SHARE_TAPPED, shareIntent); break; case IMAGE: - finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED, - IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext)); + finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED, shareIntent); break; } } @@ -744,11 +744,11 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv switch (mClipboardModel.getType()) { case TEXT: finish(CLIPBOARD_OVERLAY_EDIT_TAPPED, - IntentCreator.getTextEditorIntent(mContext)); + mIntentCreator.getTextEditorIntent(mContext)); break; case IMAGE: finishWithSharedTransition(CLIPBOARD_OVERLAY_EDIT_TAPPED, - IntentCreator.getImageEditIntent(mClipboardModel.getUri(), mContext)); + mIntentCreator.getImageEditIntent(mClipboardModel.getUri(), mContext)); break; default: Log.w(TAG, "Got preview tapped callback for non-editable type " diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/DefaultIntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/DefaultIntentCreator.java new file mode 100644 index 000000000000..4b24536ad28f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/DefaultIntentCreator.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; + +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.res.R; + +import javax.inject.Inject; + +@SysUISingleton +public class DefaultIntentCreator implements IntentCreator { + private static final String EXTRA_EDIT_SOURCE = "edit_source"; + private static final String EDIT_SOURCE_CLIPBOARD = "clipboard"; + private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY"; + + @Inject + public DefaultIntentCreator() {} + + public Intent getTextEditorIntent(Context context) { + Intent intent = new Intent(context, EditTextActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return intent; + } + + public Intent getShareIntent(ClipData clipData, Context context) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + + // From the ACTION_SEND docs: + // "If using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it should be the + // MIME type of the data in EXTRA_STREAM" + Uri uri = clipData.getItemAt(0).getUri(); + if (uri != null) { + // We don't use setData here because some apps interpret this as "to:". + shareIntent.setType(clipData.getDescription().getMimeType(0)); + // Include URI in ClipData also, so that grantPermission picks it up. + shareIntent.setClipData(new ClipData( + new ClipDescription( + "content", new String[]{clipData.getDescription().getMimeType(0)}), + new ClipData.Item(uri))); + shareIntent.putExtra(Intent.EXTRA_STREAM, uri); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } else { + shareIntent.putExtra( + Intent.EXTRA_TEXT, clipData.getItemAt(0).coerceToText(context).toString()); + shareIntent.setType("text/plain"); + } + Intent chooserIntent = Intent.createChooser(shareIntent, null) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + return chooserIntent; + } + + public Intent getImageEditIntent(Uri uri, Context context) { + String editorPackage = context.getString(R.string.config_screenshotEditor); + Intent editIntent = new Intent(Intent.ACTION_EDIT); + if (!TextUtils.isEmpty(editorPackage)) { + editIntent.setComponent(ComponentName.unflattenFromString(editorPackage)); + } + editIntent.setDataAndType(uri, "image/*"); + editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + editIntent.putExtra(EXTRA_EDIT_SOURCE, EDIT_SOURCE_CLIPBOARD); + return editIntent; + } + + public Intent getRemoteCopyIntent(ClipData clipData, Context context) { + Intent nearbyIntent = new Intent(REMOTE_COPY_ACTION); + + String remoteCopyPackage = context.getString(R.string.config_remoteCopyPackage); + if (!TextUtils.isEmpty(remoteCopyPackage)) { + nearbyIntent.setComponent(ComponentName.unflattenFromString(remoteCopyPackage)); + } + + nearbyIntent.setClipData(clipData); + nearbyIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return nearbyIntent; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java index a18b4c84b081..c8a6b05f090b 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 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. @@ -17,79 +17,13 @@ package com.android.systemui.clipboardoverlay; import android.content.ClipData; -import android.content.ClipDescription; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.text.TextUtils; -import com.android.systemui.res.R; - -class IntentCreator { - private static final String EXTRA_EDIT_SOURCE = "edit_source"; - private static final String EDIT_SOURCE_CLIPBOARD = "clipboard"; - private static final String REMOTE_COPY_ACTION = "android.intent.action.REMOTE_COPY"; - - static Intent getTextEditorIntent(Context context) { - Intent intent = new Intent(context, EditTextActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - return intent; - } - - static Intent getShareIntent(ClipData clipData, Context context) { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - - // From the ACTION_SEND docs: - // "If using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it should be the - // MIME type of the data in EXTRA_STREAM" - Uri uri = clipData.getItemAt(0).getUri(); - if (uri != null) { - // We don't use setData here because some apps interpret this as "to:". - shareIntent.setType(clipData.getDescription().getMimeType(0)); - // Include URI in ClipData also, so that grantPermission picks it up. - shareIntent.setClipData(new ClipData( - new ClipDescription( - "content", new String[]{clipData.getDescription().getMimeType(0)}), - new ClipData.Item(uri))); - shareIntent.putExtra(Intent.EXTRA_STREAM, uri); - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } else { - shareIntent.putExtra( - Intent.EXTRA_TEXT, clipData.getItemAt(0).coerceToText(context).toString()); - shareIntent.setType("text/plain"); - } - Intent chooserIntent = Intent.createChooser(shareIntent, null) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - - return chooserIntent; - } - - static Intent getImageEditIntent(Uri uri, Context context) { - String editorPackage = context.getString(R.string.config_screenshotEditor); - Intent editIntent = new Intent(Intent.ACTION_EDIT); - if (!TextUtils.isEmpty(editorPackage)) { - editIntent.setComponent(ComponentName.unflattenFromString(editorPackage)); - } - editIntent.setDataAndType(uri, "image/*"); - editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - editIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - editIntent.putExtra(EXTRA_EDIT_SOURCE, EDIT_SOURCE_CLIPBOARD); - return editIntent; - } - - static Intent getRemoteCopyIntent(ClipData clipData, Context context) { - Intent nearbyIntent = new Intent(REMOTE_COPY_ACTION); - - String remoteCopyPackage = context.getString(R.string.config_remoteCopyPackage); - if (!TextUtils.isEmpty(remoteCopyPackage)) { - nearbyIntent.setComponent(ComponentName.unflattenFromString(remoteCopyPackage)); - } - - nearbyIntent.setClipData(clipData); - nearbyIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - return nearbyIntent; - } +public interface IntentCreator { + Intent getTextEditorIntent(Context context); + Intent getShareIntent(ClipData clipData, Context context); + Intent getImageEditIntent(Uri uri, Context context); + Intent getRemoteCopyIntent(ClipData clipData, Context context); } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java index 6c10eea07ffc..c86a84b17efe 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static com.android.systemui.Flags.clipboardOverlayMultiuser; import static com.android.systemui.Flags.enableViewCaptureTracing; +import static com.android.systemui.shared.Flags.usePreferredImageEditor; import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -32,7 +33,10 @@ import android.view.WindowManager; import com.android.app.viewcapture.ViewCapture; import com.android.app.viewcapture.ViewCaptureAwareWindowManager; +import com.android.systemui.clipboardoverlay.ActionIntentCreator; import com.android.systemui.clipboardoverlay.ClipboardOverlayView; +import com.android.systemui.clipboardoverlay.DefaultIntentCreator; +import com.android.systemui.clipboardoverlay.IntentCreator; import com.android.systemui.res.R; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; @@ -102,6 +106,17 @@ public interface ClipboardOverlayModule { /* isViewCaptureEnabled= */ enableViewCaptureTracing()); } + @Provides + static IntentCreator provideIntentCreator( + Lazy<DefaultIntentCreator> defaultIntentCreator, + Lazy<ActionIntentCreator> actionIntentCreator) { + if (usePreferredImageEditor()) { + return actionIntentCreator.get(); + } else { + return defaultIntentCreator.get(); + } + } + @Qualifier @Documented @Retention(RUNTIME) diff --git a/packages/SystemUI/src/com/android/systemui/common/domain/interactor/SysUIStateDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/SysUIStateDisplaysInteractor.kt index 097d50bb8f9d..9db7b50905f8 100644 --- a/packages/SystemUI/src/com/android/systemui/common/domain/interactor/SysUIStateDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/SysUIStateDisplaysInteractor.kt @@ -16,7 +16,9 @@ package com.android.systemui.common.domain.interactor +import android.util.Log import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.PerDisplayRepository import com.android.systemui.model.StateChange import com.android.systemui.model.SysUiState @@ -26,20 +28,36 @@ import javax.inject.Inject @SysUISingleton class SysUIStateDisplaysInteractor @Inject -constructor(private val sysUIStateRepository: PerDisplayRepository<SysUiState>) { +constructor( + private val sysUIStateRepository: PerDisplayRepository<SysUiState>, + private val displayRepository: DisplayRepository, +) { /** * Sets the flags on the given [targetDisplayId] based on the [stateChanges], while making sure * that those flags are not set in any other display. */ fun setFlagsExclusivelyToDisplay(targetDisplayId: Int, stateChanges: StateChange) { - sysUIStateRepository.forEachInstance { displayId, instance -> - if (displayId == targetDisplayId) { - stateChanges.applyTo(instance) - } else { - stateChanges.clearAllChangedFlagsIn(instance) - } + if (SysUiState.DEBUG) { + Log.d(TAG, "Setting flags $stateChanges only for display $targetDisplayId") } + displayRepository.displays.value + .mapNotNull { sysUIStateRepository[it.displayId] } + .apply { + // Let's first modify all states, without committing changes ... + forEach { displaySysUIState -> + if (displaySysUIState.displayId == targetDisplayId) { + stateChanges.applyTo(displaySysUIState) + } else { + stateChanges.clearFrom(displaySysUIState) + } + } + // ... And commit changes at the end + forEach { sysuiState -> sysuiState.commitUpdate() } + } } -} + private companion object { + const val TAG = "SysUIStateInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt index d53a737480a3..72159252efec 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Color.kt @@ -19,11 +19,13 @@ package com.android.systemui.common.shared.model import android.annotation.AttrRes import android.annotation.ColorInt import android.annotation.ColorRes +import androidx.compose.runtime.Stable /** * Models a color that can be either a specific [Color.Loaded] value or a resolvable theme * [Color.Attribute] */ +@Stable sealed interface Color { data class Loaded(@ColorInt val color: Int) : Color diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt index d628aca7f9e8..b7d8ae6ce153 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt @@ -18,11 +18,13 @@ package com.android.systemui.common.shared.model import android.annotation.StringRes import android.content.Context +import androidx.compose.runtime.Stable /** * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or * be a [reference][ContentDescription.Resource] to a resource. */ +@Stable sealed class ContentDescription { data class Loaded(val description: String?) : ContentDescription() diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt index e6f02457d320..2adaec21867f 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt @@ -18,11 +18,13 @@ package com.android.systemui.common.shared.model import android.annotation.DrawableRes import android.graphics.drawable.Drawable +import androidx.compose.runtime.Stable /** * Models an icon, that can either be already [loaded][Icon.Loaded] or be a [reference] * [Icon.Resource] to a resource. In case of [Loaded], the resource ID [res] is optional. */ +@Stable sealed class Icon { abstract val contentDescription: ContentDescription? diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt index 13f6bba01135..07cc136e6bc6 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt @@ -147,7 +147,7 @@ constructor( */ fun create( context: Context, - configurationController: ConfigurationController + configurationController: ConfigurationController, ): ConfigurationStateImpl } } diff --git a/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt b/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt index fa5556d44674..cedd5161a777 100644 --- a/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt @@ -23,6 +23,7 @@ import android.os.Build import android.os.UserHandle import com.android.internal.R as InternalR import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.development.data.repository.DevelopmentSettingRepository @@ -32,9 +33,12 @@ import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.utils.UserScopedService import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @SysUISingleton @@ -46,6 +50,7 @@ constructor( private val userRepository: UserRepository, private val clipboardManagerProvider: UserScopedService<ClipboardManager>, @Background private val backgroundDispatcher: CoroutineDispatcher, + @Application private val applicationScope: CoroutineScope, ) { /** @@ -53,10 +58,11 @@ constructor( * * @see DevelopmentSettingRepository.isDevelopmentSettingEnabled */ - val buildNumber: Flow<BuildNumber?> = + val buildNumber: StateFlow<BuildNumber?> = userRepository.selectedUserInfo .flatMapConcat { userInfo -> repository.isDevelopmentSettingEnabled(userInfo) } .map { enabled -> buildText.takeIf { enabled } } + .stateIn(applicationScope, WhileSubscribed(), null) private val buildText = BuildNumber( diff --git a/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt b/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt index 68c51ea80ffd..31d0471f55e2 100644 --- a/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt @@ -41,7 +41,6 @@ constructor(private val buildNumberInteractor: BuildNumberInteractor) : Exclusiv val buildNumber: BuildNumber? by hydrator.hydratedStateOf( traceName = "buildNumber", - initialValue = null, source = buildNumberInteractor.buildNumber, ) diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt index 04f245e91914..d27e33e53dbb 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt @@ -91,18 +91,6 @@ interface PerDisplayRepository<T> { /** Debug name for this repository, mainly for tracing and logging. */ val debugName: String - - /** - * Invokes the specified action on each instance held by this repository. - * - * The action will receive the displayId and the instance associated with that display. - * If there is no instance for the display, the action is not called. - */ - fun forEachInstance(action: (Int, T) -> Unit) { - displayIds.forEach { displayId -> - get(displayId)?.let { instance -> action(displayId, instance) } - } - } } /** @@ -138,10 +126,11 @@ constructor( get() = perDisplayInstances.keys private suspend fun start() { - dumpManager.registerDumpable(this) + dumpManager.registerNormalDumpable("PerDisplayRepository-${debugName}", this) displayRepository.displayIds.collectLatest { displayIds -> val toRemove = perDisplayInstances.keys - displayIds toRemove.forEach { displayId -> + Log.d(TAG, "<$debugName> destroying instance for displayId=$displayId.") perDisplayInstances.remove(displayId)?.let { instance -> (instanceProvider as? PerDisplayInstanceProviderWithTeardown)?.destroyInstance( instance @@ -159,6 +148,7 @@ constructor( // If it doesn't exist, create it and put it in the map. return perDisplayInstances.computeIfAbsent(displayId) { key -> + Log.d(TAG, "<$debugName> creating instance for displayId=$key, as it wasn't available.") val instance = traceSection({ "creating instance of $debugName for displayId=$key" }) { instanceProvider.createInstance(key) @@ -194,8 +184,13 @@ constructor( * Provides an instance of a given class **only** for the default display, even if asked for another * display. * - * This is useful in case of flag refactors: it can be provided instead of an instance of + * This is useful in case of **flag refactors**: it can be provided instead of an instance of * [PerDisplayInstanceRepositoryImpl] when a flag related to multi display refactoring is off. + * + * Note that this still requires all instances to be provided by a [PerDisplayInstanceProvider]. If + * you want to provide an existing instance instead for the default display, either implement it in + * a custom [PerDisplayInstanceProvider] (e.g. inject it in the constructor and return it if the + * displayId is zero), or use [SingleInstanceRepositoryImpl]. */ class DefaultDisplayOnlyInstanceRepositoryImpl<T>( override val debugName: String, @@ -208,3 +203,18 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>( override fun get(displayId: Int): T? = lazyDefaultDisplayInstance } + +/** + * Always returns [instance] for any display. + * + * This can be used to provide a single instance based on a flag value during a refactor. Similar to + * [DefaultDisplayOnlyInstanceRepositoryImpl], but also avoids creating the + * [PerDisplayInstanceProvider]. This is useful when you want to provide an existing instance only, + * without even instantiating a [PerDisplayInstanceProvider]. + */ +class SingleInstanceRepositoryImpl<T>(override val debugName: String, private val instance: T) : + PerDisplayRepository<T> { + override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY) + + override fun get(displayId: Int): T? = instance +} diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 12718e8bd119..9edd9dc056c7 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -94,6 +94,7 @@ import java.util.function.Consumer; public class DozeSensors { private static final String TAG = "DozeSensors"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); + private static final String KEY_DOZE_PULSE_ON_AUTH = "doze_pulse_on_auth"; private final AsyncSensorManager mSensorManager; private final AmbientDisplayConfiguration mConfig; @@ -241,7 +242,7 @@ public class DozeSensors { ), new TriggerSensor( findSensor(config.udfpsLongPressSensorType()), - "doze_pulse_on_auth", + KEY_DOZE_PULSE_ON_AUTH, true /* settingDef */, udfpsLongPressConfigured(), DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS, @@ -421,6 +422,18 @@ public class DozeSensors { && (!s.mRequiresTouchscreen || mListeningTouchScreenSensors) && (!s.mRequiresProx || mListeningProxSensors) && (!s.mRequiresAod || mListeningAodOnlySensors); + + //AOD might be turned off in visual because of BetterySaver or isAlwaysOnSuppressed(), + //but AOD isn't really turned off, in these cases, udfpsLongPressSensor should be + //unregistered. + if (!mListeningAodOnlySensors && KEY_DOZE_PULSE_ON_AUTH.equals(s.mSetting)) { + if (mConfig.alwaysOnEnabled(mSelectedUserInteractor.getSelectedUserId()) + && !mConfig.screenOffUdfpsEnabled( + mSelectedUserInteractor.getSelectedUserId())) { + listen = false; + } + } + s.setListening(listen); if (listen) { anyListening = true; diff --git a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt index ca157afb7721..293461daef7b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt @@ -91,6 +91,7 @@ object RefactorFlagUtils { * } * ```` */ + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) inline fun unsafeAssertInNewMode(isEnabled: Boolean, flagName: Any) = check(isEnabled) { "New code path not supported when $flagName is disabled." } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index 54e27a61ac78..04a0630771df 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt @@ -88,10 +88,7 @@ constructor( private fun createDialog(): Dialog { return dialogFactory.create(dialogDelegate = ShortcutCustomizationDialogDelegate()) { dialog -> - val uiState by - viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle( - initialValue = ShortcutCustomizationUiState.Inactive - ) + val uiState by viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() ShortcutCustomizationDialog( uiState = uiState, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt index 7e0fa2f125b0..c601e6e0baf6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt @@ -409,6 +409,7 @@ private fun Title(title: String) { color = MaterialTheme.colorScheme.onSurface, lineHeight = 32.sp, fontWeight = FontWeight.W400, + textAlign = TextAlign.Center, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/SysUIKeyEventHandler.kt b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/SysUIKeyEventHandler.kt index 1febc79b8241..bc65ad476c37 100644 --- a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/SysUIKeyEventHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/SysUIKeyEventHandler.kt @@ -42,7 +42,7 @@ constructor( when (event.keyCode) { KeyEvent.KEYCODE_BACK -> { - if (event.handleAction()) { + if (!backActionInteractor.isBackCallbackRegistered() && event.handleAction()) { backActionInteractor.onBackRequested() } return true diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index b4b3053cba42..d8fc21af9724 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -1847,6 +1847,7 @@ public class KeyguardViewMediator implements CoreStartable, // explicitly DO NOT want to call // mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false) // here, since that will mess with the device lock state. + mKeyguardStateController.notifyKeyguardGoingAway(false); mUpdateMonitor.dispatchKeyguardGoingAway(false); notifyStartedGoingToSleep(); @@ -2994,7 +2995,6 @@ public class KeyguardViewMediator implements CoreStartable, startKeyguardTransition(showing, aodShowing); } else { try { - mActivityTaskManagerService.setLockScreenShown(showing, aodShowing); } catch (RemoteException ignored) { } @@ -3650,30 +3650,33 @@ public class KeyguardViewMediator implements CoreStartable, return; } - try { - int flags = KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS - | KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER; + int flags = KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS + | KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER; - // If we are unlocking to the launcher, clear the snapshot so that any changes as part - // of the in-window animations are reflected. This is needed even if we're not actually - // playing in-window animations for this particular unlock since a previous unlock might - // have changed the Launcher state. - if (mKeyguardUnlockAnimationControllerLazy.get().isSupportedLauncherUnderneath()) { - flags |= KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; - } + // If we are unlocking to the launcher, clear the snapshot so that any changes as part + // of the in-window animations are reflected. This is needed even if we're not actually + // playing in-window animations for this particular unlock since a previous unlock might + // have changed the Launcher state. + if (mKeyguardUnlockAnimationControllerLazy.get().isSupportedLauncherUnderneath()) { + flags |= KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; + } - mKeyguardStateController.notifyKeyguardGoingAway(true); + mKeyguardStateController.notifyKeyguardGoingAway(true); - if (!KeyguardWmStateRefactor.isEnabled()) { - // Handled in WmLockscreenVisibilityManager. - mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); + if (!KeyguardWmStateRefactor.isEnabled()) { + // Handled in WmLockscreenVisibilityManager. + mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); + final int goingAwayFlags = flags; + mUiBgExecutor.execute(() -> { Log.d(TAG, "keyguardGoingAway requested for userId: " + mGoingAwayRequestedForUserId); - mActivityTaskManagerService.keyguardGoingAway(flags); - } - } catch (RemoteException e) { - mSurfaceBehindRemoteAnimationRequested = false; - Log.e(TAG, "Failed to report keyguardGoingAway", e); + try { + mActivityTaskManagerService.keyguardGoingAway(goingAwayFlags); + } catch (RemoteException e) { + mSurfaceBehindRemoteAnimationRequested = false; + Log.e(TAG, "Failed to report keyguardGoingAway", e); + } + }); } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java index 2c5bacb62e72..70e2413386e3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java @@ -196,6 +196,9 @@ public class WorkLockActivity extends Activity { confirmCredentialIntent.putExtra(Intent.EXTRA_INTENT, target.getIntentSender()); } + String packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME); + confirmCredentialIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); + // WorkLockActivity is started as a task overlay, so unless credential confirmation is also // started as an overlay, it won't be visible. final ActivityOptions launchOptions = ActivityOptions.makeBasic(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt index 0b587ae1f58e..c031b53ab87d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt @@ -38,9 +38,15 @@ object AlternateBouncerUdfpsViewBinder { view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { view.alpha = 0f + launch("$TAG#viewModel.accessibilityDelegateHint") { viewModel.accessibilityDelegateHint.collect { hint -> view.accessibilityHintType = hint + if (hint != DeviceEntryIconView.AccessibilityHintType.NONE) { + view.setOnClickListener { viewModel.onTapped() } + } else { + view.setOnClickListener(null) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt index acd381ec3280..9038922466df 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context import com.android.settingslib.Utils +import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor import com.android.systemui.biometrics.domain.interactor.FingerprintPropertyInteractor import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor @@ -26,6 +27,7 @@ import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shared.recents.utilities.Utilities.clamp +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -46,6 +48,8 @@ constructor( fingerprintPropertyInteractor: FingerprintPropertyInteractor, udfpsOverlayInteractor: UdfpsOverlayInteractor, alternateBouncerViewModel: AlternateBouncerViewModel, + private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + private val accessibilityInteractor: AccessibilityInteractor, ) { private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported val alpha: Flow<Float> = @@ -74,7 +78,15 @@ constructor( } } val accessibilityDelegateHint: Flow<DeviceEntryIconView.AccessibilityHintType> = - flowOf(DeviceEntryIconView.AccessibilityHintType.ENTER) + accessibilityInteractor.isEnabled.flatMapLatest { touchExplorationEnabled -> + flowOf( + if (touchExplorationEnabled) { + DeviceEntryIconView.AccessibilityHintType.BOUNCER + } else { + DeviceEntryIconView.AccessibilityHintType.NONE + } + ) + } private val fgIconColor: Flow<Int> = configurationInteractor.onAnyConfigurationChange @@ -93,6 +105,10 @@ constructor( ) } + fun onTapped() { + statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true) + } + val bgColor: Flow<Int> = deviceEntryBackgroundViewModel.color val bgAlpha: Flow<Float> = flowOf(1f) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt deleted file mode 100644 index 0cb36edfd382..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.domain.pipeline.interactor - -import android.content.Context -import android.content.Intent -import android.provider.Settings -import android.util.Log -import androidx.annotation.VisibleForTesting -import com.android.internal.jank.InteractionJankMonitor -import com.android.systemui.animation.Expandable -import com.android.systemui.broadcast.BroadcastSender -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.media.controls.data.repository.MediaFilterRepository -import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor -import com.android.systemui.media.controls.shared.model.MediaRecModel -import com.android.systemui.media.controls.shared.model.MediaRecommendationsModel -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData -import com.android.systemui.plugins.ActivityStarter -import java.net.URISyntaxException -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.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -/** Encapsulates business logic for media recommendation */ -@SysUISingleton -class MediaRecommendationsInteractor -@Inject -constructor( - @Application applicationScope: CoroutineScope, - @Application private val applicationContext: Context, - private val repository: MediaFilterRepository, - private val mediaDataProcessor: MediaDataProcessor, - private val broadcastSender: BroadcastSender, - private val activityStarter: ActivityStarter, -) { - - val recommendations: Flow<MediaRecommendationsModel> = - repository.smartspaceMediaData.map { toRecommendationsModel(it) }.distinctUntilChanged() - - /** Indicates whether the recommendations card is active. */ - val isActive: StateFlow<Boolean> = - repository.smartspaceMediaData - .map { it.isActive } - .distinctUntilChanged() - .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) - - fun removeMediaRecommendations(key: String, dismissIntent: Intent?, delayMs: Long) { - mediaDataProcessor.dismissSmartspaceRecommendation(key, delayMs) - if (dismissIntent == null) { - Log.w(TAG, "Cannot create dismiss action click action: extras missing dismiss_intent.") - return - } - - val className = dismissIntent.component?.className - if (className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) { - // Dismiss the card Smartspace data through Smartspace trampoline activity. - applicationContext.startActivity(dismissIntent) - } else { - broadcastSender.sendBroadcast(dismissIntent) - } - } - - fun startSettings() { - activityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */ true) - } - - fun startClickIntent(expandable: Expandable, intent: Intent) { - if (shouldActivityOpenInForeground(intent)) { - // Request to unlock the device if the activity needs to be opened in foreground. - activityStarter.postStartActivityDismissingKeyguard( - intent, - 0 /* delay */, - expandable.activityTransitionController( - InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER - ), - ) - } else { - // Otherwise, open the activity in background directly. - applicationContext.startActivity(intent) - } - } - - /** Returns if the action will open the activity in foreground. */ - private fun shouldActivityOpenInForeground(intent: Intent): Boolean { - val intentString = intent.extras?.getString(EXTRAS_SMARTSPACE_INTENT) ?: return false - try { - val wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME) - return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false) - } catch (e: URISyntaxException) { - Log.wtf(TAG, "Failed to create intent from URI: $intentString") - e.printStackTrace() - } - return false - } - - private fun toRecommendationsModel(data: SmartspaceMediaData): MediaRecommendationsModel { - val mediaRecs = ArrayList<MediaRecModel>() - data.recommendations.forEach { - with(it) { mediaRecs.add(MediaRecModel(intent, title, subtitle, icon, extras)) } - } - return with(data) { - MediaRecommendationsModel( - key = targetId, - uid = getUid(applicationContext), - packageName = packageName, - instanceId = instanceId, - appName = getAppName(applicationContext), - dismissIntent = dismissIntent, - areRecommendationsValid = isValid(), - mediaRecs = mediaRecs, - ) - } - } - - fun switchToMediaControl(packageName: String) { - repository.setMediaFromRecPackageName(packageName) - } - - companion object { - - private const val TAG = "MediaRecommendationsInteractor" - - // TODO (b/237284176) : move AGSA reference out. - private const val EXTRAS_SMARTSPACE_INTENT = - "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT" - @VisibleForTesting - const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = - "com.google.android.apps.gsa.staticplugins.opa.smartspace." + - "ExportedSmartspaceTrampolineActivity" - - private const val KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND" - - private val SETTINGS_INTENT = Intent(Settings.ACTION_MEDIA_CONTROLS_SETTINGS) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaRecommendationsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaRecommendationsViewBinder.kt deleted file mode 100644 index 4877d18de7ab..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/MediaRecommendationsViewBinder.kt +++ /dev/null @@ -1,469 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.ui.binder - -import android.app.WallpaperColors -import android.content.Context -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.graphics.Bitmap -import android.graphics.Color -import android.graphics.Matrix -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.graphics.drawable.GradientDrawable -import android.graphics.drawable.Icon -import android.graphics.drawable.LayerDrawable -import android.os.Trace -import android.util.TypedValue -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.content.res.AppCompatResources -import androidx.constraintlayout.widget.ConstraintSet -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.animation.Expandable -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.media.controls.shared.model.NUM_REQUIRED_RECOMMENDATIONS -import com.android.systemui.media.controls.ui.animation.surfaceFromScheme -import com.android.systemui.media.controls.ui.animation.textPrimaryFromScheme -import com.android.systemui.media.controls.ui.animation.textSecondaryFromScheme -import com.android.systemui.media.controls.ui.controller.MediaViewController -import com.android.systemui.media.controls.ui.util.MediaArtworkHelper -import com.android.systemui.media.controls.ui.view.RecommendationViewHolder -import com.android.systemui.media.controls.ui.viewmodel.MediaRecViewModel -import com.android.systemui.media.controls.ui.viewmodel.MediaRecommendationsViewModel -import com.android.systemui.media.controls.ui.viewmodel.MediaRecsCardViewModel -import com.android.systemui.monet.ColorScheme -import com.android.systemui.monet.Style -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.res.R -import com.android.systemui.util.animation.TransitionLayout -import kotlin.math.min -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.collectLatest -import com.android.app.tracing.coroutines.launchTraced as launch -import kotlinx.coroutines.withContext - -private const val TAG = "MediaRecommendationsViewBinder" -private const val MEDIA_REC_SCRIM_START_ALPHA = 0.15f -private const val MEDIA_REC_SCRIM_END_ALPHA = 1.0f - -object MediaRecommendationsViewBinder { - - /** Binds recommendations view holder to the given view-model */ - fun bind( - viewHolder: RecommendationViewHolder, - viewModel: MediaRecommendationsViewModel, - mediaViewController: MediaViewController, - falsingManager: FalsingManager, - backgroundDispatcher: CoroutineDispatcher, - mainDispatcher: CoroutineDispatcher, - ) { - mediaViewController.recsConfigurationChangeListener = this::updateRecommendationsVisibility - val cardView = viewHolder.recommendations - cardView.repeatWhenAttached { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.mediaRecsCard.collectLatest { viewModel -> - viewModel?.let { - bindRecsCard( - viewHolder, - it, - mediaViewController, - falsingManager, - backgroundDispatcher, - mainDispatcher, - ) - } - } - } - } - } - } - } - - suspend fun bindRecsCard( - viewHolder: RecommendationViewHolder, - viewModel: MediaRecsCardViewModel, - viewController: MediaViewController, - falsingManager: FalsingManager, - backgroundDispatcher: CoroutineDispatcher, - mainDispatcher: CoroutineDispatcher, - ) { - // Set up media control location and its listener. - viewModel.onLocationChanged(viewController.currentEndLocation) - viewController.locationChangeListener = viewModel.onLocationChanged - - // Bind main card. - viewHolder.recommendations.contentDescription = - viewModel.contentDescription.invoke(viewController.isGutsVisible) - - viewHolder.recommendations.setOnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener - viewModel.onClicked(Expandable.fromView(it)) - } - - viewHolder.recommendations.setOnLongClickListener { - if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) - return@setOnLongClickListener true - if (!viewController.isGutsVisible) { - openGuts(viewHolder, viewModel, viewController) - } else { - closeGuts(viewHolder, viewModel, viewController) - } - return@setOnLongClickListener true - } - - // Bind colors - val appIcon = viewModel.mediaRecs.first().appIcon - fetchAndUpdateColors(viewHolder, appIcon, backgroundDispatcher, mainDispatcher) - // Bind all recommendations. - bindRecommendationsList( - viewHolder, - viewModel.mediaRecs, - falsingManager, - backgroundDispatcher, - mainDispatcher, - ) - updateRecommendationsVisibility(viewController, viewHolder.recommendations) - - // Set visibility of recommendations. - val expandedSet: ConstraintSet = viewController.expandedLayout - val collapsedSet: ConstraintSet = viewController.collapsedLayout - viewHolder.mediaTitles.forEach { - setVisibleAndAlpha(expandedSet, it.id, viewModel.areTitlesVisible) - setVisibleAndAlpha(collapsedSet, it.id, viewModel.areTitlesVisible) - } - viewHolder.mediaSubtitles.forEach { - setVisibleAndAlpha(expandedSet, it.id, viewModel.areSubtitlesVisible) - setVisibleAndAlpha(collapsedSet, it.id, viewModel.areSubtitlesVisible) - } - - bindRecommendationsGuts(viewHolder, viewModel, viewController, falsingManager) - - viewController.refreshState() - } - - private fun bindRecommendationsGuts( - viewHolder: RecommendationViewHolder, - viewModel: MediaRecsCardViewModel, - viewController: MediaViewController, - falsingManager: FalsingManager, - ) { - val gutsViewHolder = viewHolder.gutsViewHolder - val gutsViewModel = viewModel.gutsMenu - - gutsViewHolder.gutsText.text = gutsViewModel.gutsText - gutsViewHolder.dismissText.visibility = View.VISIBLE - gutsViewHolder.dismiss.isEnabled = true - gutsViewHolder.dismiss.setOnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener - closeGuts(viewHolder, viewModel, viewController) - gutsViewModel.onDismissClicked() - } - - gutsViewHolder.cancelText.background = gutsViewModel.cancelTextBackground - gutsViewHolder.cancel.setOnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { - closeGuts(viewHolder, viewModel, viewController) - } - } - - gutsViewHolder.settings.setOnClickListener { - if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { - gutsViewModel.onSettingsClicked.invoke() - } - } - - gutsViewHolder.setDismissible(gutsViewModel.isDismissEnabled) - } - - private suspend fun bindRecommendationsList( - viewHolder: RecommendationViewHolder, - mediaRecs: List<MediaRecViewModel>, - falsingManager: FalsingManager, - backgroundDispatcher: CoroutineDispatcher, - mainDispatcher: CoroutineDispatcher, - ) { - mediaRecs.forEachIndexed { index, mediaRecViewModel -> - if (index >= NUM_REQUIRED_RECOMMENDATIONS) return@forEachIndexed - - val appIconView = viewHolder.mediaAppIcons[index] - appIconView.clearColorFilter() - appIconView.setImageDrawable(mediaRecViewModel.appIcon) - - val mediaCoverContainer = viewHolder.mediaCoverContainers[index] - mediaCoverContainer.setOnClickListener { - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@setOnClickListener - mediaRecViewModel.onClicked(Expandable.fromView(it), index) - } - mediaCoverContainer.setOnLongClickListener { - if (falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) - return@setOnLongClickListener true - (it.parent as View).performLongClick() - return@setOnLongClickListener true - } - - val mediaCover = viewHolder.mediaCoverItems[index] - bindRecommendationArtwork( - mediaCover.context, - viewHolder, - mediaRecViewModel, - index, - backgroundDispatcher, - mainDispatcher, - ) - mediaCover.contentDescription = mediaRecViewModel.contentDescription - - val title = viewHolder.mediaTitles[index] - title.text = mediaRecViewModel.title - - val subtitle = viewHolder.mediaSubtitles[index] - subtitle.text = mediaRecViewModel.subtitle - - val progressBar = viewHolder.mediaProgressBars[index] - progressBar.progress = mediaRecViewModel.progress - if (mediaRecViewModel.progress == 0) { - progressBar.visibility = View.GONE - } - } - } - - private fun openGuts( - viewHolder: RecommendationViewHolder, - viewModel: MediaRecsCardViewModel, - mediaViewController: MediaViewController, - ) { - viewHolder.marquee(true, MediaViewController.GUTS_ANIMATION_DURATION) - mediaViewController.openGuts() - viewHolder.recommendations.contentDescription = viewModel.contentDescription.invoke(true) - viewModel.onLongClicked.invoke() - } - - private fun closeGuts( - viewHolder: RecommendationViewHolder, - mediaRecsCardViewModel: MediaRecsCardViewModel, - mediaViewController: MediaViewController, - ) { - viewHolder.marquee(false, MediaViewController.GUTS_ANIMATION_DURATION) - mediaViewController.closeGuts(false) - viewHolder.recommendations.contentDescription = - mediaRecsCardViewModel.contentDescription.invoke(false) - } - - private fun setVisibleAndAlpha(set: ConstraintSet, resId: Int, visible: Boolean) { - set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else ConstraintSet.GONE) - set.setAlpha(resId, if (visible) 1.0f else 0.0f) - } - - fun updateRecommendationsVisibility( - mediaViewController: MediaViewController, - cardView: TransitionLayout, - ) { - val fittedRecsNum = getNumberOfFittedRecommendations(cardView.context) - val expandedSet = mediaViewController.expandedLayout - val collapsedSet = mediaViewController.collapsedLayout - val mediaCoverContainers = getMediaCoverContainers(cardView) - // Hide media cover that cannot fit in the recommendation card. - mediaCoverContainers.forEachIndexed { index, container -> - setVisibleAndAlpha(expandedSet, container.id, index < fittedRecsNum) - setVisibleAndAlpha(collapsedSet, container.id, index < fittedRecsNum) - } - } - - private fun getMediaCoverContainers(cardView: TransitionLayout): List<ViewGroup> { - return listOf<ViewGroup>( - cardView.requireViewById(R.id.media_cover1_container), - cardView.requireViewById(R.id.media_cover2_container), - cardView.requireViewById(R.id.media_cover3_container), - ) - } - - private fun getNumberOfFittedRecommendations(context: Context): Int { - val res = context.resources - val config = res.configuration - val defaultDpWidth = res.getInteger(R.integer.default_qs_media_rec_width_dp) - val recCoverWidth = - (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) + - res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2) - - // On landscape, media controls should take half of the screen width. - val displayAvailableDpWidth = - if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { - config.screenWidthDp / 2 - } else { - config.screenWidthDp - } - val fittedNum = - if (displayAvailableDpWidth > defaultDpWidth) { - val recCoverDefaultWidth = - res.getDimensionPixelSize(R.dimen.qs_media_rec_default_width) - recCoverDefaultWidth / recCoverWidth - } else { - val displayAvailableWidth = - TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - displayAvailableDpWidth.toFloat(), - res.displayMetrics, - ) - .toInt() - displayAvailableWidth / recCoverWidth - } - return min(fittedNum.toDouble(), NUM_REQUIRED_RECOMMENDATIONS.toDouble()).toInt() - } - - private suspend fun bindRecommendationArtwork( - context: Context, - viewHolder: RecommendationViewHolder, - viewModel: MediaRecViewModel, - index: Int, - backgroundDispatcher: CoroutineDispatcher, - mainDispatcher: CoroutineDispatcher, - ) { - val traceCookie = viewHolder.hashCode() - val traceName = "MediaRecommendationsViewBinder#bindRecommendationArtwork" - Trace.beginAsyncSection(traceName, traceCookie) - - // Capture width & height from views in foreground for artwork scaling in background - val width = context.resources.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) - val height = - context.resources.getDimensionPixelSize(R.dimen.qs_media_rec_album_height_expanded) - - withContext(backgroundDispatcher) { - val artwork = - getRecCoverBackground( - context, - viewModel.albumIcon, - width, - height, - backgroundDispatcher, - ) - withContext(mainDispatcher) { - val mediaCover = viewHolder.mediaCoverItems[index] - val coverMatrix = Matrix(mediaCover.imageMatrix) - coverMatrix.postScale(1.25f, 1.25f, 0.5f * width, 0.5f * height) - mediaCover.imageMatrix = coverMatrix - mediaCover.setImageDrawable(artwork) - } - } - } - - /** Returns the recommendation album cover of [width]x[height] size. */ - private suspend fun getRecCoverBackground( - context: Context, - icon: Icon?, - width: Int, - height: Int, - backgroundDispatcher: CoroutineDispatcher, - ): Drawable = - withContext(backgroundDispatcher) { - return@withContext MediaArtworkHelper.getWallpaperColor( - context, - backgroundDispatcher, - icon, - TAG, - ) - ?.let { wallpaperColors -> - addGradientToRecommendationAlbum( - context, - icon!!, - ColorScheme(wallpaperColors, true, Style.CONTENT), - width, - height, - ) - } ?: ColorDrawable(Color.TRANSPARENT) - } - - private fun addGradientToRecommendationAlbum( - context: Context, - artworkIcon: Icon, - mutableColorScheme: ColorScheme, - width: Int, - height: Int, - ): LayerDrawable { - // First try scaling rec card using bitmap drawable. - // If returns null, set drawable bounds. - val albumArt = - getScaledRecommendationCover(context, artworkIcon, width, height) - ?: MediaArtworkHelper.getScaledBackground(context, artworkIcon, width, height) - val gradient = - AppCompatResources.getDrawable(context, R.drawable.qs_media_rec_scrim)?.mutate() - as GradientDrawable - return MediaArtworkHelper.setUpGradientColorOnDrawable( - albumArt, - gradient, - mutableColorScheme, - MEDIA_REC_SCRIM_START_ALPHA, - MEDIA_REC_SCRIM_END_ALPHA, - ) - } - - /** Returns a [Drawable] of a given [artworkIcon] scaled to [width]x[height] size, . */ - private fun getScaledRecommendationCover( - context: Context, - artworkIcon: Icon, - width: Int, - height: Int, - ): Drawable? { - check(width > 0) { "Width must be a positive number but was $width" } - check(height > 0) { "Height must be a positive number but was $height" } - - return if ( - artworkIcon.type == Icon.TYPE_BITMAP || artworkIcon.type == Icon.TYPE_ADAPTIVE_BITMAP - ) { - artworkIcon.bitmap?.let { - val bitmap = Bitmap.createScaledBitmap(it, width, height, false) - BitmapDrawable(context.resources, bitmap) - } - } else { - null - } - } - - private suspend fun fetchAndUpdateColors( - viewHolder: RecommendationViewHolder, - appIcon: Drawable, - backgroundDispatcher: CoroutineDispatcher, - mainDispatcher: CoroutineDispatcher, - ) = - withContext(backgroundDispatcher) { - val colorScheme = - ColorScheme(WallpaperColors.fromDrawable(appIcon), /* darkTheme= */ true) - withContext(mainDispatcher) { - val backgroundColor = surfaceFromScheme(colorScheme) - val textPrimaryColor = textPrimaryFromScheme(colorScheme) - val textSecondaryColor = textSecondaryFromScheme(colorScheme) - - viewHolder.cardTitle.setTextColor(textPrimaryColor) - viewHolder.recommendations.setBackgroundTintList( - ColorStateList.valueOf(backgroundColor) - ) - - viewHolder.mediaTitles.forEach { it.setTextColor(textPrimaryColor) } - viewHolder.mediaSubtitles.forEach { it.setTextColor(textSecondaryColor) } - viewHolder.mediaProgressBars.forEach { - it.progressTintList = ColorStateList.valueOf(textPrimaryColor) - } - - viewHolder.gutsViewHolder.setColors(colorScheme) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 7b1ae57ed421..ac6343c6bb64 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -61,14 +61,12 @@ import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder -import com.android.systemui.media.controls.ui.binder.MediaRecommendationsViewBinder import com.android.systemui.media.controls.ui.util.MediaViewModelCallback import com.android.systemui.media.controls.ui.util.MediaViewModelListUpdateCallback import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.controls.ui.view.MediaScrollView import com.android.systemui.media.controls.ui.view.MediaViewHolder -import com.android.systemui.media.controls.ui.view.RecommendationViewHolder import com.android.systemui.media.controls.ui.viewmodel.MediaCarouselViewModel import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel import com.android.systemui.media.controls.util.MediaUiEventLogger @@ -478,41 +476,10 @@ constructor( MediaPlayerData.isSwipedAway = false } - override fun onSmartspaceMediaDataLoaded( - key: String, - data: SmartspaceMediaData, - shouldPrioritize: Boolean, - ) { - debugLogger.logRecommendationLoaded(key, data.isActive) - // Log the case where the hidden media carousel with the existed inactive resume - // media is shown by the Smartspace signal. - if (data.isActive) { - addSmartspaceMediaRecommendations(key, data, shouldPrioritize) - } else { - // Handle update to inactive as a removal - onSmartspaceMediaDataRemoved(data.targetId, immediately = true) - } - MediaPlayerData.isSwipedAway = false - } - override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { debugLogger.logMediaRemoved(key, userInitiated) removePlayer(key, userInitiated = userInitiated) } - - override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - debugLogger.logRecommendationRemoved(key, immediately) - if (immediately || isReorderingAllowed) { - removePlayer(key) - if (!immediately) { - // Although it wasn't requested, we were able to process the removal - // immediately since reordering is allowed. So, notify hosts to update - updateHostVisibility() - } - } else { - keysNeedRemoval.add(key) - } - } } ) } @@ -655,22 +622,6 @@ constructor( mediaContent.addView(viewHolder.player, position) controllerById[commonViewModel.instanceId.toString()] = viewController } - is MediaCommonViewModel.MediaRecommendations -> { - val viewHolder = - RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent) - viewController.attachRecommendations(viewHolder) - viewController.recommendationViewHolder?.recommendations?.layoutParams = lp - MediaRecommendationsViewBinder.bind( - viewHolder, - commonViewModel.recsViewModel, - viewController, - falsingManager, - backgroundDispatcher, - mainDispatcher, - ) - mediaContent.addView(viewHolder.recommendations, position) - controllerById[commonViewModel.key] = viewController - } } viewController.setListening(mediaCarouselScrollHandler.visibleToUser && currentlyExpanded) updateViewControllerToState(viewController, noAnimation = true) @@ -695,21 +646,10 @@ constructor( } private fun onRemoved(commonViewModel: MediaCommonViewModel) { - val id = - when (commonViewModel) { - is MediaCommonViewModel.MediaControl -> commonViewModel.instanceId.toString() - is MediaCommonViewModel.MediaRecommendations -> commonViewModel.key - } + val id = (commonViewModel as MediaCommonViewModel.MediaControl).instanceId.toString() controllerById.remove(id)?.let { - when (commonViewModel) { - is MediaCommonViewModel.MediaControl -> { - mediaCarouselScrollHandler.onPrePlayerRemoved(it.mediaViewHolder!!.player) - mediaContent.removeView(it.mediaViewHolder!!.player) - } - is MediaCommonViewModel.MediaRecommendations -> { - mediaContent.removeView(it.recommendationViewHolder!!.recommendations) - } - } + mediaCarouselScrollHandler.onPrePlayerRemoved(it.mediaViewHolder!!.player) + mediaContent.removeView(it.mediaViewHolder!!.player) it.onDestroy() mediaCarouselScrollHandler.onPlayersChanged() updatePageIndicator() @@ -718,21 +658,10 @@ constructor( } private fun onMoved(commonViewModel: MediaCommonViewModel, from: Int, to: Int) { - val id = - when (commonViewModel) { - is MediaCommonViewModel.MediaControl -> commonViewModel.instanceId.toString() - is MediaCommonViewModel.MediaRecommendations -> commonViewModel.key - } + val id = (commonViewModel as MediaCommonViewModel.MediaControl).instanceId.toString() controllerById[id]?.let { mediaContent.removeViewAt(from) - when (commonViewModel) { - is MediaCommonViewModel.MediaControl -> { - mediaContent.addView(it.mediaViewHolder!!.player, to) - } - is MediaCommonViewModel.MediaRecommendations -> { - mediaContent.addView(it.recommendationViewHolder!!.recommendations, to) - } - } + mediaContent.addView(it.mediaViewHolder!!.player, to) } updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() @@ -746,11 +675,9 @@ constructor( val viewIds = viewModels .map { mediaCommonViewModel -> - when (mediaCommonViewModel) { - is MediaCommonViewModel.MediaControl -> - mediaCommonViewModel.instanceId.toString() - is MediaCommonViewModel.MediaRecommendations -> mediaCommonViewModel.key - } + (mediaCommonViewModel as MediaCommonViewModel.MediaControl) + .instanceId + .toString() } .toHashSet() controllerById @@ -758,7 +685,6 @@ constructor( .forEach { mediaCarouselScrollHandler.onPrePlayerRemoved(it.value.mediaViewHolder?.player) mediaContent.removeView(it.value.mediaViewHolder?.player) - mediaContent.removeView(it.value.recommendationViewHolder?.recommendations) it.value.onDestroy() mediaCarouselScrollHandler.onPlayersChanged() updatePageIndicator() @@ -808,9 +734,6 @@ constructor( mediaContent.removeAllViews() for (mediaPlayer in MediaPlayerData.players()) { mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) } - ?: mediaPlayer.recommendationViewHolder?.let { - mediaContent.addView(it.recommendations) - } } mediaCarouselScrollHandler.onPlayersChanged() mediaControlChipInteractor.updateMediaControlChipModelLegacy( @@ -980,67 +903,6 @@ constructor( return MediaViewHolder.create(LayoutInflater.from(context), mediaContent) } - private fun addSmartspaceMediaRecommendations( - key: String, - data: SmartspaceMediaData, - shouldPrioritize: Boolean, - ) = - traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") { - if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") - MediaPlayerData.getMediaPlayer(key)?.let { - Log.w(TAG, "Skip adding smartspace target in carousel") - return - } - - val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() - existingSmartspaceMediaKey?.let { - val removedPlayer = - removePlayer(existingSmartspaceMediaKey, dismissMediaData = false) - removedPlayer?.run { - debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) - onDestroy() - } - } - - val newRecs = mediaControlPanelFactory.get() - newRecs.attachRecommendation( - RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent) - ) - newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions - val lp = - LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - ) - newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp) - newRecs.bindRecommendation(data) - val curVisibleMediaKey = - MediaPlayerData.visiblePlayerKeys() - .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) - MediaPlayerData.addMediaRecommendation( - key, - data, - newRecs, - shouldPrioritize, - systemClock, - debugLogger, - ) - updateViewControllerToState(newRecs.mediaViewController, noAnimation = true) - reorderAllPlayers(curVisibleMediaKey) - updatePageIndicator() - mediaFrame.requiresRemeasuring = true - // Check postcondition: mediaContent should have the same number of children as there - // are elements in mediaPlayers. - if (MediaPlayerData.players().size != mediaContent.childCount) { - Log.e( - TAG, - "Size of players list and number of views in carousel are out of sync. " + - "Players size is ${MediaPlayerData.players().size}. " + - "View count is ${mediaContent.childCount}.", - ) - } - } - fun removePlayer( key: String, dismissMediaData: Boolean = true, @@ -1057,7 +919,6 @@ constructor( return removed?.apply { mediaCarouselScrollHandler.onPrePlayerRemoved(removed.mediaViewHolder?.player) mediaContent.removeView(removed.mediaViewHolder?.player) - mediaContent.removeView(removed.recommendationViewHolder?.recommendations) removed.onDestroy() mediaCarouselScrollHandler.onPlayersChanged() mediaControlChipInteractor.updateMediaControlChipModelLegacy( @@ -1095,31 +956,18 @@ constructor( val mediaDataList = MediaPlayerData.mediaData() // Do not loop through the original list of media data because the re-addition of media data // is being executed in background thread. - mediaDataList.forEach { (key, data, isSsMediaRec) -> - if (isSsMediaRec) { - val smartspaceMediaData = MediaPlayerData.smartspaceMediaData + mediaDataList.forEach { (key, data, _) -> + val isSsReactivated = MediaPlayerData.isSsReactivated(key) + if (recreateMedia) { removePlayer(key, dismissMediaData = false, dismissRecommendation = false) - smartspaceMediaData?.let { - addSmartspaceMediaRecommendations( - it.targetId, - it, - MediaPlayerData.shouldPrioritizeSs, - ) - } - onUiExecutionEnd.run() - } else { - val isSsReactivated = MediaPlayerData.isSsReactivated(key) - if (recreateMedia) { - removePlayer(key, dismissMediaData = false, dismissRecommendation = false) - } - addOrUpdatePlayer( - key = key, - oldKey = null, - data = data, - isSsReactivated = isSsReactivated, - onUiExecutionEnd = onUiExecutionEnd, - ) } + addOrUpdatePlayer( + key = key, + oldKey = null, + data = data, + isSsReactivated = isSsReactivated, + onUiExecutionEnd = onUiExecutionEnd, + ) } } @@ -1129,12 +977,8 @@ constructor( if (recreateMedia) { mediaContent.removeAllViews() commonViewModels.forEachIndexed { index, viewModel -> - when (viewModel) { - is MediaCommonViewModel.MediaControl -> - controllerById[viewModel.instanceId.toString()]?.onDestroy() - is MediaCommonViewModel.MediaRecommendations -> - controllerById[viewModel.key]?.onDestroy() - } + val mediaControlViewModel = (viewModel as MediaCommonViewModel.MediaControl) + controllerById[mediaControlViewModel.instanceId.toString()]?.onDestroy() onAdded(viewModel, index, configChanged = true) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLogger.kt index 5d62c022efba..365389107648 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLogger.kt @@ -64,28 +64,6 @@ constructor(@MediaCarouselControllerLog private val buffer: LogBuffer) { { "removing player $str1, by user $bool1" }, ) - fun logRecommendationLoaded(key: String, isActive: Boolean) = - buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - bool1 = isActive - }, - { "add recommendation $str1, active $bool1" }, - ) - - fun logRecommendationRemoved(key: String, immediately: Boolean) = - buffer.log( - TAG, - LogLevel.DEBUG, - { - str1 = key - bool1 = immediately - }, - { "removing recommendation $str1, immediate=$bool1" }, - ) - fun logCarouselHidden() = buffer.log(TAG, LogLevel.DEBUG, {}, { "hiding carousel" }) fun logCarouselVisible() = buffer.log(TAG, LogLevel.DEBUG, {}, { "showing carousel" }) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index a6bf5f43698b..006eb203a669 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -22,7 +22,6 @@ import static com.android.settingslib.flags.Flags.legacyLeAudioSharing; import static com.android.systemui.Flags.communalHub; import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation; import static com.android.systemui.media.controls.domain.pipeline.MediaActionsKt.getNotificationActions; -import static com.android.systemui.media.controls.shared.model.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS; import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA; import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY; import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_START_ALPHA; @@ -35,24 +34,17 @@ import android.app.ActivityOptions; import android.app.BroadcastOptions; import android.app.PendingIntent; import android.app.WallpaperColors; -import android.app.smartspace.SmartspaceAction; import android.content.Context; import android.content.Intent; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BlendMode; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; -import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Animatable; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; @@ -69,14 +61,12 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.NonNull; @@ -105,7 +95,6 @@ import com.android.systemui.media.controls.shared.model.MediaAction; import com.android.systemui.media.controls.shared.model.MediaButton; import com.android.systemui.media.controls.shared.model.MediaData; import com.android.systemui.media.controls.shared.model.MediaDeviceData; -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData; import com.android.systemui.media.controls.ui.animation.AnimationBindHandler; import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition; import com.android.systemui.media.controls.ui.animation.MediaColorSchemesKt; @@ -113,7 +102,6 @@ import com.android.systemui.media.controls.ui.animation.MetadataAnimationHandler import com.android.systemui.media.controls.ui.binder.SeekBarObserver; import com.android.systemui.media.controls.ui.view.GutsViewHolder; import com.android.systemui.media.controls.ui.view.MediaViewHolder; -import com.android.systemui.media.controls.ui.view.RecommendationViewHolder; import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel; import com.android.systemui.media.controls.util.MediaDataUtils; import com.android.systemui.media.controls.util.MediaUiEventLogger; @@ -143,14 +131,12 @@ import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.settings.GlobalSettings; -import com.android.systemui.util.time.SystemClock; import dagger.Lazy; import kotlin.Triple; import kotlin.Unit; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -165,17 +151,6 @@ public class MediaControlPanel { protected static final String TAG = "MediaControlPanel"; private static final float DISABLED_ALPHA = 0.38f; - private static final String EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = "com.google" - + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity"; - private static final String EXTRAS_SMARTSPACE_INTENT = - "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT"; - private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name"; - private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND"; - - private static final float REC_MEDIA_COVER_SCALE_FACTOR = 1.25f; - private static final float MEDIA_REC_SCRIM_START_ALPHA = 0.15f; - private static final float MEDIA_REC_SCRIM_END_ALPHA = 1.0f; - private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS); // Buttons to show in small player when using semantic actions @@ -215,17 +190,14 @@ public class MediaControlPanel { private Context mContext; private MediaViewHolder mMediaViewHolder; - private RecommendationViewHolder mRecommendationViewHolder; private String mKey; private MediaData mMediaData; - private SmartspaceMediaData mRecommendationData; private MediaViewController mMediaViewController; private MediaSession.Token mToken; private MediaController mController; private Lazy<MediaDataManager> mMediaDataManagerLazy; // Uid for the media app. protected int mUid = Process.INVALID_UID; - private int mSmartspaceMediaItemsCount; private MediaCarouselController mMediaCarouselController; private final MediaOutputDialogManager mMediaOutputDialogManager; private final FalsingManager mFalsingManager; @@ -241,7 +213,6 @@ public class MediaControlPanel { private final NotificationLockscreenUserManager mLockscreenUserManager; // Used for logging. - private SystemClock mSystemClock; private MediaUiEventLogger mLogger; private InstanceId mInstanceId; private String mPackageName; @@ -310,7 +281,6 @@ public class MediaControlPanel { MediaOutputDialogManager mediaOutputDialogManager, MediaCarouselController mediaCarouselController, FalsingManager falsingManager, - SystemClock systemClock, MediaUiEventLogger logger, KeyguardStateController keyguardStateController, ActivityIntentHelper activityIntentHelper, @@ -330,7 +300,6 @@ public class MediaControlPanel { mMediaOutputDialogManager = mediaOutputDialogManager; mMediaCarouselController = mediaCarouselController; mFalsingManager = falsingManager; - mSystemClock = systemClock; mLogger = logger; mKeyguardStateController = keyguardStateController; mActivityIntentHelper = activityIntentHelper; @@ -373,16 +342,6 @@ public class MediaControlPanel { } /** - * Get the recommendation view holder used to display Smartspace media recs. - * - * @return the recommendation view holder - */ - @Nullable - public RecommendationViewHolder getRecommendationViewHolder() { - return mRecommendationViewHolder; - } - - /** * Get the view controller used to display media controls * * @return the media view controller @@ -465,7 +424,7 @@ public class MediaControlPanel { mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener); mSeekBarViewModel.setEnabledChangeListener(mEnabledChangeListener); - mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER); + mMediaViewController.attach(player); vh.getPlayer().setOnLongClickListener(v -> { if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true; @@ -522,26 +481,6 @@ public class MediaControlPanel { return result; } - /** Attaches the recommendations to the recommendation view holder. */ - public void attachRecommendation(RecommendationViewHolder vh) { - mRecommendationViewHolder = vh; - TransitionLayout recommendations = vh.getRecommendations(); - - mMediaViewController.attach(recommendations, MediaViewController.TYPE.RECOMMENDATION); - mMediaViewController.configurationChangeListener = this::updateRecommendationsVisibility; - - mRecommendationViewHolder.getRecommendations().setOnLongClickListener(v -> { - if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true; - if (!mMediaViewController.isGutsVisible()) { - openGuts(); - return true; - } else { - closeGuts(); - return true; - } - }); - } - /** Bind this player view based on the data given. */ public void bindPlayer(@NonNull MediaData data, String key) { SceneContainerFlag.assertInLegacyMode(); @@ -868,24 +807,6 @@ public class MediaControlPanel { mMediaViewHolder.getPlayer().setContentDescription(contentDescription); } - private void bindRecommendationContentDescription(SmartspaceMediaData data) { - if (mRecommendationViewHolder == null) { - return; - } - - CharSequence contentDescription; - if (mMediaViewController.isGutsVisible()) { - contentDescription = - mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText(); - } else if (data != null) { - contentDescription = mContext.getString(R.string.controls_media_smartspace_rec_header); - } else { - contentDescription = null; - } - - mRecommendationViewHolder.getRecommendations().setContentDescription(contentDescription); - } - private void bindArtworkAndColors(MediaData data, String key, boolean updateBackground) { final int traceCookie = data.hashCode(); final String traceName = "MediaControlPanel#bindArtworkAndColors<" + key + ">"; @@ -993,62 +914,6 @@ public class MediaControlPanel { }); } - private void bindRecommendationArtwork( - SmartspaceAction recommendation, - String packageName, - int itemIndex - ) { - final int traceCookie = recommendation.hashCode(); - final String traceName = - "MediaControlPanel#bindRecommendationArtwork<" + packageName + ">"; - Trace.beginAsyncSection(traceName, traceCookie); - - // Capture width & height from views in foreground for artwork scaling in background - int width = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_rec_album_width); - int height = mContext.getResources().getDimensionPixelSize( - R.dimen.qs_media_rec_album_height_expanded); - - mBackgroundExecutor.execute(() -> { - // Album art - ColorScheme mutableColorScheme = null; - Drawable artwork; - Icon artworkIcon = recommendation.getIcon(); - WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon); - if (wallpaperColors != null) { - mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); - artwork = addGradientToRecommendationAlbum(artworkIcon, mutableColorScheme, width, - height); - } else { - artwork = new ColorDrawable(Color.TRANSPARENT); - } - - mMainExecutor.execute(() -> { - // Bind the artwork drawable to media cover. - ImageView mediaCover = - mRecommendationViewHolder.getMediaCoverItems().get(itemIndex); - // Rescale media cover - Matrix coverMatrix = new Matrix(mediaCover.getImageMatrix()); - coverMatrix.postScale(REC_MEDIA_COVER_SCALE_FACTOR, REC_MEDIA_COVER_SCALE_FACTOR, - 0.5f * width, 0.5f * height); - mediaCover.setImageMatrix(coverMatrix); - mediaCover.setImageDrawable(artwork); - - // Set up the app icon. - ImageView appIconView = mRecommendationViewHolder.getMediaAppIcons().get(itemIndex); - appIconView.clearColorFilter(); - try { - Drawable icon = mContext.getPackageManager() - .getApplicationIcon(packageName); - appIconView.setImageDrawable(icon); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Cannot find icon for package " + packageName, e); - appIconView.setImageResource(R.drawable.ic_music_note); - } - Trace.endAsyncSection(traceName, traceCookie); - }); - }); - } - // This method should be called from a background thread. WallpaperColors.fromBitmap takes a // good amount of time. We do that work on the background executor to avoid stalling animations // on the UI Thread. @@ -1088,21 +953,6 @@ public class MediaControlPanel { MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY, MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY); } - @VisibleForTesting - protected LayerDrawable addGradientToRecommendationAlbum(Icon artworkIcon, - ColorScheme mutableColorScheme, int width, int height) { - // First try scaling rec card using bitmap drawable. - // If returns null, set drawable bounds. - Drawable albumArt = getScaledRecommendationCover(artworkIcon, width, height); - if (albumArt == null) { - albumArt = getScaledBackground(artworkIcon, width, height); - } - GradientDrawable gradient = (GradientDrawable) mContext.getDrawable( - R.drawable.qs_media_rec_scrim).mutate(); - return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme, - MEDIA_REC_SCRIM_START_ALPHA, MEDIA_REC_SCRIM_END_ALPHA); - } - private LayerDrawable setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient, ColorScheme mutableColorScheme, float startAlpha, float endAlpha) { int startColor; @@ -1465,258 +1315,6 @@ public class MediaControlPanel { return controller; } - /** Bind this recommendation view based on the given data. */ - public void bindRecommendation(@NonNull SmartspaceMediaData data) { - if (mRecommendationViewHolder == null) { - return; - } - - if (!data.isValid()) { - Log.e(TAG, "Received an invalid recommendation list; returning"); - return; - } - - if (Trace.isEnabled()) { - Trace.traceBegin(Trace.TRACE_TAG_APP, - "MediaControlPanel#bindRecommendation<" + data.getPackageName() + ">"); - } - - mRecommendationData = data; - mPackageName = data.getPackageName(); - mInstanceId = data.getInstanceId(); - - // Set up recommendation card's header. - ApplicationInfo applicationInfo; - try { - applicationInfo = mContext.getPackageManager() - .getApplicationInfo(data.getPackageName(), 0 /* flags */); - mUid = applicationInfo.uid; - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Fail to get media recommendation's app info", e); - Trace.endSection(); - return; - } - - CharSequence appName = data.getAppName(mContext); - if (appName == null) { - Log.w(TAG, "Fail to get media recommendation's app name"); - Trace.endSection(); - return; - } - - PackageManager packageManager = mContext.getPackageManager(); - // Set up media source app's logo. - Drawable icon = packageManager.getApplicationIcon(applicationInfo); - fetchAndUpdateRecommendationColors(icon); - - // Set up media rec card's tap action if applicable. - TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations(); - setSmartspaceRecItemOnClickListener(recommendationCard, data.getCardAction(), - /* interactedSubcardRank */ -1); - bindRecommendationContentDescription(data); - - List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems(); - List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers(); - List<SmartspaceAction> recommendations = data.getValidRecommendations(); - - boolean hasTitle = false; - boolean hasSubtitle = false; - int fittedRecsNum = getNumberOfFittedRecommendations(); - for (int itemIndex = 0; itemIndex < NUM_REQUIRED_RECOMMENDATIONS; itemIndex++) { - SmartspaceAction recommendation = recommendations.get(itemIndex); - - // Set up media item cover. - ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex); - bindRecommendationArtwork(recommendation, data.getPackageName(), itemIndex); - - // Set up the media item's click listener if applicable. - ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex); - setSmartspaceRecItemOnClickListener(mediaCoverContainer, recommendation, itemIndex); - // Bubble up the long-click event to the card. - mediaCoverContainer.setOnLongClickListener(v -> { - if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true; - View parent = (View) v.getParent(); - if (parent != null) { - parent.performLongClick(); - } - return true; - }); - - // Set up the accessibility label for the media item. - String artistName = recommendation.getExtras() - .getString(KEY_SMARTSPACE_ARTIST_NAME, ""); - if (artistName.isEmpty()) { - mediaCoverImageView.setContentDescription( - mContext.getString( - R.string.controls_media_smartspace_rec_item_no_artist_description, - recommendation.getTitle(), appName)); - } else { - mediaCoverImageView.setContentDescription( - mContext.getString( - R.string.controls_media_smartspace_rec_item_description, - recommendation.getTitle(), artistName, appName)); - } - - // Set up title - CharSequence title = recommendation.getTitle(); - hasTitle |= !TextUtils.isEmpty(title); - TextView titleView = mRecommendationViewHolder.getMediaTitles().get(itemIndex); - titleView.setText(title); - - // Set up subtitle - // It would look awkward to show a subtitle if we don't have a title. - boolean shouldShowSubtitleText = !TextUtils.isEmpty(title); - CharSequence subtitle = shouldShowSubtitleText ? recommendation.getSubtitle() : ""; - hasSubtitle |= !TextUtils.isEmpty(subtitle); - TextView subtitleView = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex); - subtitleView.setText(subtitle); - - // Set up progress bar - SeekBar mediaProgressBar = - mRecommendationViewHolder.getMediaProgressBars().get(itemIndex); - TextView mediaSubtitle = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex); - // show progress bar if the recommended album is played. - Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras()); - if (progress == null || progress <= 0.0) { - mediaProgressBar.setVisibility(View.GONE); - mediaSubtitle.setVisibility(View.VISIBLE); - } else { - mediaProgressBar.setProgress((int) (progress * 100)); - mediaProgressBar.setVisibility(View.VISIBLE); - mediaSubtitle.setVisibility(View.GONE); - } - } - mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS; - - // If there's no subtitles and/or titles for any of the albums, hide those views. - ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); - ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); - final boolean titlesVisible = hasTitle; - final boolean subtitlesVisible = hasSubtitle; - mRecommendationViewHolder.getMediaTitles().forEach((titleView) -> { - setVisibleAndAlpha(expandedSet, titleView.getId(), titlesVisible); - setVisibleAndAlpha(collapsedSet, titleView.getId(), titlesVisible); - }); - mRecommendationViewHolder.getMediaSubtitles().forEach((subtitleView) -> { - setVisibleAndAlpha(expandedSet, subtitleView.getId(), subtitlesVisible); - setVisibleAndAlpha(collapsedSet, subtitleView.getId(), subtitlesVisible); - }); - - // Media covers visibility. - setMediaCoversVisibility(fittedRecsNum); - - // Guts - Runnable onDismissClickedRunnable = () -> { - closeGuts(); - mMediaDataManagerLazy.get().dismissSmartspaceRecommendation( - data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L); - - Intent dismissIntent = data.getDismissIntent(); - if (dismissIntent == null) { - Log.w(TAG, "Cannot create dismiss action click action: " - + "extras missing dismiss_intent."); - return; - } - - if (dismissIntent.getComponent() != null - && dismissIntent.getComponent().getClassName() - .equals(EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME)) { - // Dismiss the card Smartspace data through Smartspace trampoline activity. - mContext.startActivity(dismissIntent); - } else { - mBroadcastSender.sendBroadcast(dismissIntent); - } - }; - bindGutsMenuCommon( - /* isDismissible= */ true, - appName.toString(), - mRecommendationViewHolder.getGutsViewHolder(), - onDismissClickedRunnable); - - mController = null; - if (mMetadataAnimationHandler == null || !mMetadataAnimationHandler.isRunning()) { - mMediaViewController.refreshState(); - } - Trace.endSection(); - } - - private Unit updateRecommendationsVisibility() { - int fittedRecsNum = getNumberOfFittedRecommendations(); - setMediaCoversVisibility(fittedRecsNum); - return Unit.INSTANCE; - } - - private void setMediaCoversVisibility(int fittedRecsNum) { - ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); - ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); - List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers(); - // Hide media cover that cannot fit in the recommendation card. - for (int itemIndex = 0; itemIndex < NUM_REQUIRED_RECOMMENDATIONS; itemIndex++) { - setVisibleAndAlpha(expandedSet, mediaCoverContainers.get(itemIndex).getId(), - itemIndex < fittedRecsNum); - setVisibleAndAlpha(collapsedSet, mediaCoverContainers.get(itemIndex).getId(), - itemIndex < fittedRecsNum); - } - } - - @VisibleForTesting - protected int getNumberOfFittedRecommendations() { - Resources res = mContext.getResources(); - Configuration config = res.getConfiguration(); - int defaultDpWidth = res.getInteger(R.integer.default_qs_media_rec_width_dp); - int recCoverWidth = res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) - + res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2; - - // On landscape, media controls should take half of the screen width. - int displayAvailableDpWidth = config.screenWidthDp; - if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { - displayAvailableDpWidth = displayAvailableDpWidth / 2; - } - int fittedNum; - if (displayAvailableDpWidth > defaultDpWidth) { - int recCoverDefaultWidth = res.getDimensionPixelSize( - R.dimen.qs_media_rec_default_width); - fittedNum = recCoverDefaultWidth / recCoverWidth; - } else { - int displayAvailableWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - displayAvailableDpWidth, res.getDisplayMetrics()); - fittedNum = displayAvailableWidth / recCoverWidth; - } - return Math.min(fittedNum, NUM_REQUIRED_RECOMMENDATIONS); - } - - private void fetchAndUpdateRecommendationColors(Drawable appIcon) { - mBackgroundExecutor.execute(() -> { - ColorScheme colorScheme = new ColorScheme( - WallpaperColors.fromDrawable(appIcon), /* darkTheme= */ true); - mMainExecutor.execute(() -> setRecommendationColors(colorScheme)); - }); - } - - private void setRecommendationColors(ColorScheme colorScheme) { - if (mRecommendationViewHolder == null) { - return; - } - - int backgroundColor = MediaColorSchemesKt.surfaceFromScheme(colorScheme); - int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme); - int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme); - - mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor); - - mRecommendationViewHolder.getRecommendations() - .setBackgroundTintList(ColorStateList.valueOf(backgroundColor)); - mRecommendationViewHolder.getMediaTitles().forEach( - (title) -> title.setTextColor(textPrimaryColor)); - mRecommendationViewHolder.getMediaSubtitles().forEach( - (subtitle) -> subtitle.setTextColor(textSecondaryColor)); - mRecommendationViewHolder.getMediaProgressBars().forEach( - (progressBar) -> progressBar.setProgressTintList( - ColorStateList.valueOf(textPrimaryColor))); - - mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme); - } - private void bindGutsMenuCommon( boolean isDismissible, String appName, @@ -1772,14 +1370,10 @@ public class MediaControlPanel { public void closeGuts(boolean immediate) { if (mMediaViewHolder != null) { mMediaViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION); - } else if (mRecommendationViewHolder != null) { - mRecommendationViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION); } mMediaViewController.closeGuts(immediate); if (mMediaViewHolder != null) { bindPlayerContentDescription(mMediaData); - } else if (mRecommendationViewHolder != null) { - bindRecommendationContentDescription(mRecommendationData); } } @@ -1790,14 +1384,10 @@ public class MediaControlPanel { private void openGuts() { if (mMediaViewHolder != null) { mMediaViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION); - } else if (mRecommendationViewHolder != null) { - mRecommendationViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION); } mMediaViewController.openGuts(); if (mMediaViewHolder != null) { bindPlayerContentDescription(mMediaData); - } else if (mRecommendationViewHolder != null) { - bindRecommendationContentDescription(mRecommendationData); } mLogger.logLongPressOpen(mUid, mPackageName, mInstanceId); } @@ -1822,29 +1412,6 @@ public class MediaControlPanel { } /** - * Scale artwork to fill the background of media covers in recommendation card. - */ - @UiThread - private Drawable getScaledRecommendationCover(Icon artworkIcon, int width, int height) { - if (width == 0 || height == 0) { - return null; - } - if (artworkIcon != null) { - Bitmap bitmap; - if (artworkIcon.getType() == Icon.TYPE_BITMAP - || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) { - Bitmap artworkBitmap = artworkIcon.getBitmap(); - if (artworkBitmap != null) { - bitmap = Bitmap.createScaledBitmap(artworkIcon.getBitmap(), width, - height, false); - return new BitmapDrawable(mContext.getResources(), bitmap); - } - } - } - return null; - } - - /** * Get the current media controller * * @return the controller @@ -1896,64 +1463,5 @@ public class MediaControlPanel { set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : notVisibleValue); set.setAlpha(actionId, visible ? 1.0f : 0.0f); } - - private void setSmartspaceRecItemOnClickListener( - @NonNull View view, - @NonNull SmartspaceAction action, - int interactedSubcardRank) { - if (view == null || action == null || action.getIntent() == null - || action.getIntent().getExtras() == null) { - Log.e(TAG, "No tap action can be set up"); - return; - } - - view.setOnClickListener(v -> { - if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return; - - if (interactedSubcardRank == -1) { - mLogger.logRecommendationCardTap(mPackageName, mInstanceId); - } else { - mLogger.logRecommendationItemTap(mPackageName, mInstanceId, interactedSubcardRank); - } - - if (shouldSmartspaceRecItemOpenInForeground(action)) { - // Request to unlock the device if the activity needs to be opened in foreground. - mActivityStarter.postStartActivityDismissingKeyguard( - action.getIntent(), - 0 /* delay */, - buildLaunchAnimatorController( - mRecommendationViewHolder.getRecommendations())); - } else { - // Otherwise, open the activity in background directly. - view.getContext().startActivity(action.getIntent()); - } - - // Automatically scroll to the active player once the media is loaded. - mMediaCarouselController.setShouldScrollToKey(true); - }); - } - - /** Returns if the Smartspace action will open the activity in foreground. */ - private boolean shouldSmartspaceRecItemOpenInForeground(SmartspaceAction action) { - if (action == null || action.getIntent() == null - || action.getIntent().getExtras() == null) { - return false; - } - - String intentString = action.getIntent().getExtras().getString(EXTRAS_SMARTSPACE_INTENT); - if (intentString == null) { - return false; - } - - try { - Intent wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME); - return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false); - } catch (URISyntaxException e) { - Log.wtf(TAG, "Failed to create intent from URI: " + intentString); - e.printStackTrace(); - } - - return false; - } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index b687dce20b06..dba190022c8b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -38,7 +38,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition import com.android.systemui.media.controls.ui.animation.MetadataAnimationHandler import com.android.systemui.media.controls.ui.binder.MediaControlViewBinder -import com.android.systemui.media.controls.ui.binder.MediaRecommendationsViewBinder import com.android.systemui.media.controls.ui.binder.SeekBarObserver import com.android.systemui.media.controls.ui.controller.MediaCarouselController.Companion.calculateAlpha import com.android.systemui.media.controls.ui.view.GutsViewHolder @@ -48,7 +47,6 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder.Companion.hea import com.android.systemui.media.controls.ui.view.MediaViewHolder.Companion.labelLargeTF import com.android.systemui.media.controls.ui.view.MediaViewHolder.Companion.labelMediumTF import com.android.systemui.media.controls.ui.view.MediaViewHolder.Companion.titleMediumTF -import com.android.systemui.media.controls.ui.view.RecommendationViewHolder import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.res.R @@ -90,15 +88,6 @@ constructor( private val globalSettings: GlobalSettings, ) { - /** - * Indicating that the media view controller is for a notification-based player, session-based - * player, or recommendation - */ - enum class TYPE { - PLAYER, - RECOMMENDATION, - } - companion object { @JvmField val GUTS_ANIMATION_DURATION = 234L } @@ -115,7 +104,6 @@ constructor( private var animationDuration: Long = 0 private var animateNextStateChange: Boolean = false private val measurement = MeasurementOutput(0, 0) - private var type: TYPE = TYPE.PLAYER /** A map containing all viewStates for all locations of this mediaState */ private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf() @@ -203,7 +191,6 @@ constructor( private var isNextButtonAvailable = false /** View holders for controller */ - var recommendationViewHolder: RecommendationViewHolder? = null var mediaViewHolder: MediaViewHolder? = null private lateinit var seekBarObserver: SeekBarObserver @@ -417,13 +404,9 @@ constructor( /** Set the height of UMO background constraints. */ private fun setBackgroundHeights(height: Int) { - val backgroundIds = - if (type == TYPE.PLAYER) { - MediaViewHolder.backgroundIds - } else { - setOf(RecommendationViewHolder.backgroundId) - } - backgroundIds.forEach { id -> expandedLayout.getConstraint(id).layout.mHeight = height } + MediaViewHolder.backgroundIds.forEach { id -> + expandedLayout.getConstraint(id).layout.mHeight = height + } } /** @@ -431,11 +414,7 @@ constructor( * [TransitionViewState]. */ private fun setGutsViewState(viewState: TransitionViewState) { - val controlsIds = - when (type) { - TYPE.PLAYER -> MediaViewHolder.controlsIds - TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds - } + val controlsIds = MediaViewHolder.controlsIds val gutsIds = GutsViewHolder.ids controlsIds.forEach { id -> viewState.widgetStates.get(id)?.let { state -> @@ -467,7 +446,6 @@ constructor( squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight } } - // media player calculateWidgetGroupAlphaForSquishiness( MediaViewHolder.expandedBottomActionIds, squishedViewState.measureHeight.toFloat(), @@ -480,20 +458,6 @@ constructor( squishedViewState, squishFraction, ) - // recommendation card - val titlesTop = - calculateWidgetGroupAlphaForSquishiness( - RecommendationViewHolder.mediaTitlesAndSubtitlesIds, - squishedViewState.measureHeight.toFloat(), - squishedViewState, - squishFraction, - ) - calculateWidgetGroupAlphaForSquishiness( - RecommendationViewHolder.mediaContainersIds, - titlesTop, - squishedViewState, - squishFraction, - ) return squishedViewState } @@ -661,10 +625,10 @@ constructor( * Attach a view to this controller. This may perform measurements if it's not available yet and * should therefore be done carefully. */ - fun attach(transitionLayout: TransitionLayout, type: TYPE) = + fun attach(transitionLayout: TransitionLayout) = traceSection("MediaViewController#attach") { - loadLayoutForType(type) - logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation) + loadLayoutConstraints() + logger.logMediaLocation("attach", currentStartLocation, currentEndLocation) this.transitionLayout = transitionLayout layoutController.attach(transitionLayout) if (currentEndLocation == MediaHierarchyManager.LOCATION_UNKNOWN) { @@ -691,7 +655,7 @@ constructor( seekBarViewModel.setEnabledChangeListener(enabledChangeListener) val mediaCard = mediaViewHolder.player - attach(mediaViewHolder.player, TYPE.PLAYER) + attach(mediaViewHolder.player) val turbulenceNoiseView = mediaViewHolder.turbulenceNoiseView turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) @@ -813,15 +777,6 @@ constructor( } } - fun attachRecommendations(recommendationViewHolder: RecommendationViewHolder) { - if (!SceneContainerFlag.isEnabled) return - this.recommendationViewHolder = recommendationViewHolder - - attach(recommendationViewHolder.recommendations, TYPE.RECOMMENDATION) - recsConfigurationChangeListener = - MediaRecommendationsViewBinder::updateRecommendationsVisibility - } - fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) { if (!SceneContainerFlag.isEnabled) return seekBarViewModel.logSeek = onSeek @@ -1026,20 +981,10 @@ constructor( return result } - private fun loadLayoutForType(type: TYPE) { - this.type = type - - // These XML resources contain ConstraintSets that will apply to this player type's layout - when (type) { - TYPE.PLAYER -> { - collapsedLayout.load(context, R.xml.media_session_collapsed) - expandedLayout.load(context, R.xml.media_session_expanded) - } - TYPE.RECOMMENDATION -> { - collapsedLayout.load(context, R.xml.media_recommendations_collapsed) - expandedLayout.load(context, R.xml.media_recommendations_expanded) - } - } + private fun loadLayoutConstraints() { + // These XML resources contain ConstraintSets that will apply to this player's layout + collapsedLayout.load(context, R.xml.media_session_collapsed) + expandedLayout.load(context, R.xml.media_session_expanded) readjustUIUpdateConstraints() refreshState() } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt index f28edd638b10..2fc44ad3cce6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelCallback.kt @@ -42,8 +42,7 @@ class MediaViewModelCallback( ) { oldItem.instanceId == newItem.instanceId } else { - oldItem is MediaCommonViewModel.MediaRecommendations && - newItem is MediaCommonViewModel.MediaRecommendations + false } } @@ -56,11 +55,6 @@ class MediaViewModelCallback( ) { oldItem.immediatelyUpdateUi == newItem.immediatelyUpdateUi && oldItem.updateTime == newItem.updateTime - } else if ( - oldItem is MediaCommonViewModel.MediaRecommendations && - newItem is MediaCommonViewModel.MediaRecommendations - ) { - oldItem.key == newItem.key && oldItem.loadingEnabled == newItem.loadingEnabled } else { false } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/RecommendationViewHolder.kt deleted file mode 100644 index 2d028d0213ff..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/RecommendationViewHolder.kt +++ /dev/null @@ -1,122 +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.systemui.media.controls.ui.view - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.SeekBar -import android.widget.TextView -import com.android.internal.widget.CachingIconView -import com.android.systemui.media.controls.ui.drawable.IlluminationDrawable -import com.android.systemui.res.R -import com.android.systemui.util.animation.TransitionLayout - -private const val TAG = "RecommendationViewHolder" - -/** ViewHolder for a Smartspace media recommendation. */ -class RecommendationViewHolder private constructor(itemView: View) { - - val recommendations = itemView as TransitionLayout - - // Recommendation screen - val cardTitle: TextView = itemView.requireViewById(R.id.media_rec_title) - - val mediaCoverContainers = - listOf<ViewGroup>( - itemView.requireViewById(R.id.media_cover1_container), - itemView.requireViewById(R.id.media_cover2_container), - itemView.requireViewById(R.id.media_cover3_container) - ) - val mediaAppIcons: List<CachingIconView> = - mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) } - val mediaTitles: List<TextView> = - mediaCoverContainers.map { it.requireViewById(R.id.media_title) } - val mediaSubtitles: List<TextView> = - mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) } - val mediaProgressBars: List<SeekBar> = - mediaCoverContainers.map { - it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply { - // Media playback is in the direction of tape, not time, so it stays LTR - layoutDirection = View.LAYOUT_DIRECTION_LTR - } - } - - val mediaCoverItems: List<ImageView> = - mediaCoverContainers.map { it.requireViewById(R.id.media_cover) } - val gutsViewHolder = GutsViewHolder(itemView) - - init { - (recommendations.background as IlluminationDrawable).let { background -> - mediaCoverContainers.forEach { background.registerLightSource(it) } - background.registerLightSource(gutsViewHolder.cancel) - background.registerLightSource(gutsViewHolder.dismiss) - background.registerLightSource(gutsViewHolder.settings) - } - } - - fun marquee(start: Boolean, delay: Long) { - gutsViewHolder.marquee(start, delay, TAG) - } - - companion object { - /** - * Creates a RecommendationViewHolder. - * - * @param inflater LayoutInflater to use to inflate the layout. - * @param parent Parent of inflated view. - */ - @JvmStatic - fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder { - val itemView = - inflater.inflate(R.layout.media_recommendations, parent, false /* attachToRoot */) - // Because this media view (a TransitionLayout) is used to measure and layout the views - // in various states before being attached to its parent, we can't depend on the default - // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction. - itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE - return RecommendationViewHolder(itemView) - } - - // Res Ids for the control components on the recommendation view. - val controlsIds = - setOf( - R.id.media_rec_title, - R.id.media_cover, - R.id.media_cover1_container, - R.id.media_cover2_container, - R.id.media_cover3_container, - R.id.media_title, - R.id.media_subtitle, - ) - - val mediaTitlesAndSubtitlesIds = - setOf( - R.id.media_title, - R.id.media_subtitle, - ) - - val mediaContainersIds = - setOf( - R.id.media_cover1_container, - R.id.media_cover2_container, - R.id.media_cover3_container - ) - - val backgroundId = R.id.sizing_view - } -} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt index e5f1766fbb28..dfaee4434bcf 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt @@ -49,7 +49,6 @@ constructor( private val visualStabilityProvider: VisualStabilityProvider, private val interactor: MediaCarouselInteractor, private val controlInteractorFactory: MediaControlInteractorFactory, - private val recommendationsViewModel: MediaRecommendationsViewModel, private val logger: MediaUiEventLogger, private val mediaLogger: MediaLogger, ) { @@ -69,7 +68,7 @@ constructor( when (commonModel) { is MediaCommonModel.MediaControl -> add(toViewModel(commonModel)) is MediaCommonModel.MediaRecommendations -> - add(toViewModel(commonModel)) + return@forEach // TODO(b/382680767): remove } } } @@ -95,8 +94,6 @@ constructor( private val mediaControlByInstanceId = mutableMapOf<InstanceId, MediaCommonViewModel.MediaControl>() - private var mediaRecs: MediaCommonViewModel.MediaRecommendations? = null - private var modelsPendingRemoval: MutableSet<MediaCommonModel> = mutableSetOf() private var allowReorder = false @@ -149,37 +146,6 @@ constructor( ) } - private fun toViewModel( - commonModel: MediaCommonModel.MediaRecommendations - ): MediaCommonViewModel.MediaRecommendations { - return mediaRecs?.copy( - key = commonModel.recsLoadingModel.key, - loadingEnabled = interactor.isRecommendationActive(), - ) - ?: MediaCommonViewModel.MediaRecommendations( - key = commonModel.recsLoadingModel.key, - loadingEnabled = interactor.isRecommendationActive(), - recsViewModel = recommendationsViewModel, - onAdded = { commonViewModel -> - mediaLogger.logMediaRecommendationCardAdded( - commonModel.recsLoadingModel.key - ) - onMediaRecommendationAddedOrUpdated( - commonViewModel as MediaCommonViewModel.MediaRecommendations - ) - }, - onRemoved = { immediatelyRemove -> - onMediaRecommendationRemoved(commonModel, immediatelyRemove) - }, - onUpdated = { commonViewModel -> - onMediaRecommendationAddedOrUpdated( - commonViewModel as MediaCommonViewModel.MediaRecommendations - ) - }, - ) - .also { mediaRecs = it } - } - private fun onMediaControlAddedOrUpdated( commonViewModel: MediaCommonViewModel, commonModel: MediaCommonModel.MediaControl, @@ -197,32 +163,6 @@ constructor( } } - private fun onMediaRecommendationAddedOrUpdated( - commonViewModel: MediaCommonViewModel.MediaRecommendations - ) { - if (!interactor.isRecommendationActive()) { - commonViewModel.onRemoved(true) - } - } - - private fun onMediaRecommendationRemoved( - commonModel: MediaCommonModel.MediaRecommendations, - immediatelyRemove: Boolean, - ) { - mediaLogger.logMediaRecommendationCardRemoved(commonModel.recsLoadingModel.key) - if (immediatelyRemove || isReorderingAllowed()) { - interactor.dismissSmartspaceRecommendation(commonModel.recsLoadingModel.key, 0L) - mediaRecs = null - if (!immediatelyRemove) { - // Although it wasn't requested, we were able to process the removal - // immediately since reordering is allowed. So, notify hosts to update - updateHostVisibility() - } - } else { - modelsPendingRemoval.add(commonModel) - } - } - private fun isReorderingAllowed(): Boolean { return visualStabilityProvider.isReorderingAllowed } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt index 52cb173b39cb..d493d57051f7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt @@ -35,13 +35,4 @@ sealed class MediaCommonViewModel { val isMediaFromRec: Boolean = false, val updateTime: Long = 0, ) : MediaCommonViewModel() - - data class MediaRecommendations( - val key: String, - val loadingEnabled: Boolean, - val recsViewModel: MediaRecommendationsViewModel, - override val onAdded: (MediaCommonViewModel) -> Unit, - override val onRemoved: (Boolean) -> Unit, - override val onUpdated: (MediaCommonViewModel) -> Unit, - ) : MediaCommonViewModel() } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecViewModel.kt deleted file mode 100644 index 77add4035067..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.ui.viewmodel - -import android.graphics.drawable.Drawable -import android.graphics.drawable.Icon -import com.android.systemui.animation.Expandable - -/** Models UI state for media recommendation item */ -data class MediaRecViewModel( - val contentDescription: CharSequence, - val title: CharSequence = "", - val subtitle: CharSequence = "", - /** track progress [0 - 100] for the recommendation album. */ - val progress: Int = 0, - val albumIcon: Icon? = null, - val appIcon: Drawable, - val onClicked: ((Expandable, Int) -> Unit), -) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt deleted file mode 100644 index 90313ddc736e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.ui.viewmodel - -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.os.Process -import android.util.Log -import com.android.internal.logging.InstanceId -import com.android.systemui.animation.Expandable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.media.controls.domain.pipeline.interactor.MediaRecommendationsInteractor -import com.android.systemui.media.controls.shared.model.MediaRecModel -import com.android.systemui.media.controls.shared.model.MediaRecommendationsModel -import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager -import com.android.systemui.media.controls.ui.controller.MediaLocation -import com.android.systemui.media.controls.ui.controller.MediaViewController.Companion.GUTS_ANIMATION_DURATION -import com.android.systemui.media.controls.util.MediaDataUtils -import com.android.systemui.media.controls.util.MediaUiEventLogger -import com.android.systemui.res.R -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map - -/** Models UI state and handles user input for media recommendations */ -@SysUISingleton -class MediaRecommendationsViewModel -@Inject -constructor( - @Application private val applicationContext: Context, - @Background private val backgroundDispatcher: CoroutineDispatcher, - private val interactor: MediaRecommendationsInteractor, - private val logger: MediaUiEventLogger, -) { - - val mediaRecsCard: Flow<MediaRecsCardViewModel?> = - interactor.recommendations - .map { recsCard -> toRecsViewModel(recsCard) } - .distinctUntilChanged() - .flowOn(backgroundDispatcher) - - @MediaLocation private var location = MediaHierarchyManager.LOCATION_UNKNOWN - - /** - * Called whenever the recommendation has been expired or removed by the user. This method - * removes the recommendation card entirely from the carousel. - */ - private fun onMediaRecommendationsDismissed( - key: String, - uid: Int, - packageName: String, - dismissIntent: Intent?, - instanceId: InstanceId?, - ) { - logger.logLongPressDismiss(uid, packageName, instanceId) - interactor.removeMediaRecommendations(key, dismissIntent, GUTS_DISMISS_DELAY_MS_DURATION) - } - - private fun onClicked( - expandable: Expandable, - intent: Intent?, - packageName: String, - instanceId: InstanceId?, - index: Int, - ) { - if (intent == null || intent.extras == null) { - Log.e(TAG, "No tap action can be set up") - return - } - - if (index == -1) { - logger.logRecommendationCardTap(packageName, instanceId) - } else { - logger.logRecommendationItemTap(packageName, instanceId, index) - } - - // set the package name of the player added by recommendation once the media is loaded. - interactor.switchToMediaControl(packageName) - - interactor.startClickIntent(expandable, intent) - } - - private suspend fun toRecsViewModel(model: MediaRecommendationsModel): MediaRecsCardViewModel? { - if (!model.areRecommendationsValid) { - Log.e(TAG, "Received an invalid recommendation list") - return null - } - if (model.appName == null || model.uid == Process.INVALID_UID) { - Log.w(TAG, "Fail to get media recommendation's app info") - return null - } - - val appIcon = getIconFromApp(model.packageName) ?: return null - - var areTitlesVisible = false - var areSubtitlesVisible = false - val mediaRecs = - model.mediaRecs.map { mediaRecModel -> - areTitlesVisible = areTitlesVisible || !mediaRecModel.title.isNullOrEmpty() - areSubtitlesVisible = areSubtitlesVisible || !mediaRecModel.subtitle.isNullOrEmpty() - val progress = MediaDataUtils.getDescriptionProgress(mediaRecModel.extras) ?: 0.0 - MediaRecViewModel( - contentDescription = - setUpMediaRecContentDescription(mediaRecModel, model.appName), - title = mediaRecModel.title ?: "", - subtitle = mediaRecModel.subtitle ?: "", - progress = (progress * 100).toInt(), - albumIcon = mediaRecModel.icon, - appIcon = appIcon, - onClicked = { expandable, index -> - onClicked( - expandable, - mediaRecModel.intent, - model.packageName, - model.instanceId, - index, - ) - }, - ) - } - // Subtitles should only be visible if titles are visible. - areSubtitlesVisible = areTitlesVisible && areSubtitlesVisible - - return MediaRecsCardViewModel( - contentDescription = { gutsVisible -> - if (gutsVisible) { - applicationContext.getString( - R.string.controls_media_close_session, - model.appName, - ) - } else { - applicationContext.getString(R.string.controls_media_smartspace_rec_header) - } - }, - onClicked = { expandable -> - onClicked( - expandable, - model.dismissIntent, - model.packageName, - model.instanceId, - index = -1, - ) - }, - onLongClicked = { - logger.logLongPressOpen(model.uid, model.packageName, model.instanceId) - }, - mediaRecs = mediaRecs, - areTitlesVisible = areTitlesVisible, - areSubtitlesVisible = areSubtitlesVisible, - gutsMenu = toGutsViewModel(model), - onLocationChanged = { location = it }, - ) - } - - private fun toGutsViewModel(model: MediaRecommendationsModel): GutsViewModel { - return GutsViewModel( - gutsText = - applicationContext.getString(R.string.controls_media_close_session, model.appName), - onDismissClicked = { - onMediaRecommendationsDismissed( - model.key, - model.uid, - model.packageName, - model.dismissIntent, - model.instanceId, - ) - }, - cancelTextBackground = - applicationContext.getDrawable(R.drawable.qs_media_outline_button), - onSettingsClicked = { - logger.logLongPressSettings(model.uid, model.packageName, model.instanceId) - interactor.startSettings() - }, - ) - } - - private fun setUpMediaRecContentDescription( - mediaRec: MediaRecModel, - appName: CharSequence?, - ): CharSequence { - // Set up the accessibility label for the media item. - val artistName = mediaRec.extras?.getString(KEY_SMARTSPACE_ARTIST_NAME, "") - return if (artistName.isNullOrEmpty()) { - applicationContext.getString( - R.string.controls_media_smartspace_rec_item_no_artist_description, - mediaRec.title, - appName, - ) - } else { - applicationContext.getString( - R.string.controls_media_smartspace_rec_item_description, - mediaRec.title, - artistName, - appName, - ) - } - } - - private fun getIconFromApp(packageName: String): Drawable? { - return try { - applicationContext.packageManager.getApplicationIcon(packageName) - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Cannot find icon for package $packageName", e) - null - } - } - - companion object { - private const val TAG = "MediaRecommendationsViewModel" - private const val KEY_SMARTSPACE_ARTIST_NAME = "artist_name" - /** - * Delay duration is based on [GUTS_ANIMATION_DURATION], it should have 100 ms increase in - * order to let the animation end. - */ - private const val GUTS_DISMISS_DELAY_MS_DURATION = 334L - } -} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecsCardViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecsCardViewModel.kt deleted file mode 100644 index f1f7dc2195d5..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecsCardViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.ui.viewmodel - -import com.android.systemui.animation.Expandable - -/** Models UI state for media recommendations card. */ -data class MediaRecsCardViewModel( - val contentDescription: (Boolean) -> CharSequence, - val onClicked: (Expandable) -> Unit, - val onLongClicked: () -> Unit, - val mediaRecs: List<MediaRecViewModel>, - val areTitlesVisible: Boolean, - val areSubtitlesVisible: Boolean, - val gutsMenu: GutsViewModel, - val onLocationChanged: (Int) -> Unit, -) 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 1f2f571496bd..9d375809786a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -648,10 +648,6 @@ public class MediaSwitchingController final MediaDevice connectedMediaDevice = needToHandleMutingExpectedDevice ? null : getCurrentConnectedMediaDevice(); - - Set<String> selectedDevicesIds = getSelectedMediaDevice().stream() - .map(MediaDevice::getId) - .collect(Collectors.toSet()); if (oldMediaItems.isEmpty()) { if (connectedMediaDevice == null) { if (DEBUG) { @@ -660,14 +656,12 @@ public class MediaSwitchingController return categorizeMediaItemsLocked( /* connectedMediaDevice */ null, devices, - selectedDevicesIds, needToHandleMutingExpectedDevice); } else { // selected device exist return categorizeMediaItemsLocked( connectedMediaDevice, devices, - selectedDevicesIds, /* needToHandleMutingExpectedDevice */ false); } } @@ -701,20 +695,9 @@ public class MediaSwitchingController devices.removeAll(targetMediaDevices); targetMediaDevices.addAll(devices); } - List<MediaItem> finalMediaItems = new ArrayList<>(); - boolean shouldAddFirstSeenSelectedDevice = - com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping(); - for (MediaDevice targetMediaDevice : targetMediaDevices) { - if (shouldAddFirstSeenSelectedDevice - && selectedDevicesIds.contains(targetMediaDevice.getId())) { - finalMediaItems.add(MediaItem.createDeviceMediaItem( - targetMediaDevice, /* isFirstDeviceInGroup */ true)); - shouldAddFirstSeenSelectedDevice = false; - } else { - finalMediaItems.add(MediaItem.createDeviceMediaItem( - targetMediaDevice, /* isFirstDeviceInGroup */ false)); - } - } + List<MediaItem> finalMediaItems = targetMediaDevices.stream() + .map(MediaItem::createDeviceMediaItem) + .collect(Collectors.toList()); dividerItems.forEach(finalMediaItems::add); attachConnectNewDeviceItemIfNeeded(finalMediaItems); return finalMediaItems; @@ -741,9 +724,11 @@ public class MediaSwitchingController @GuardedBy("mMediaDevicesLock") private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice, List<MediaDevice> devices, - Set<String> selectedDevicesIds, boolean needToHandleMutingExpectedDevice) { List<MediaItem> finalMediaItems = new ArrayList<>(); + Set<String> selectedDevicesIds = getSelectedMediaDevice().stream() + .map(MediaDevice::getId) + .collect(Collectors.toSet()); if (connectedMediaDevice != null) { selectedDevicesIds.add(connectedMediaDevice.getId()); } diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt index ae276707c3db..c9fb8e877009 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt @@ -14,45 +14,690 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package com.android.systemui.media.remedia.ui.compose +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderColors +import androidx.compose.material3.SliderDefaults.colors +import androidx.compose.material3.SliderState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf +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.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.Layout import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import com.android.compose.PlatformButton import com.android.compose.PlatformIconButton +import com.android.compose.PlatformOutlinedButton import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState +import com.android.compose.animation.scene.transitions import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load +import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture import com.android.systemui.media.remedia.shared.model.MediaSessionState +import com.android.systemui.media.remedia.ui.viewmodel.MediaCardGutsViewModel +import com.android.systemui.media.remedia.ui.viewmodel.MediaCardViewModel import com.android.systemui.media.remedia.ui.viewmodel.MediaOutputSwitcherChipViewModel import com.android.systemui.media.remedia.ui.viewmodel.MediaPlayPauseActionViewModel import com.android.systemui.media.remedia.ui.viewmodel.MediaSecondaryActionViewModel +import com.android.systemui.media.remedia.ui.viewmodel.MediaSeekBarViewModel +import kotlin.math.max + +/** Renders the UI of a single media card. */ +@Composable +private fun Card( + viewModel: MediaCardViewModel, + presentationStyle: MediaPresentationStyle, + modifier: Modifier = Modifier, +) { + val stlState = + rememberMutableSceneTransitionLayoutState( + initialScene = presentationStyle.toScene(), + transitions = Media.Transitions, + ) + + // Each time the presentation style changes, animate to the corresponding scene. + LaunchedEffect(presentationStyle) { + stlState.setTargetScene(targetScene = presentationStyle.toScene(), animationScope = this) + } + + Box(modifier) { + if (stlState.currentScene != Media.Scenes.Compact) { + CardBackground(imageLoader = viewModel.artLoader, modifier = Modifier.matchParentSize()) + } + + key(stlState) { + SceneTransitionLayout(state = stlState) { + scene(Media.Scenes.Default) { + CardForeground(viewModel = viewModel, threeRows = true, fillHeight = false) + } + + scene(Media.Scenes.Compressed) { + CardForeground(viewModel = viewModel, threeRows = false, fillHeight = false) + } + + scene(Media.Scenes.Compact) { CompactCardForeground(viewModel = viewModel) } + } + } + } +} + +/** + * Renders the foreground of a card, including all UI content and the internal "guts". + * + * If [threeRows] is `true`, the layout will be organized as three horizontal rows; if `false`, two + * rows will be used, resulting in a more compact layout. + * + * If [fillHeight] is `true`, the card will grow vertically to fill all available space in its + * parent. If not, it'll only be as tall as needed to show its UI. + */ +@Composable +private fun ContentScope.CardForeground( + viewModel: MediaCardViewModel, + threeRows: Boolean, + fillHeight: Boolean, + modifier: Modifier = Modifier, +) { + // Can't use a Crossfade composable because of the custom layout logic below. Animate the alpha + // of the guts (and, indirectly, of the content) from here. + val gutsAlphaAnimatable = remember { Animatable(0f) } + val isGutsVisible = viewModel.guts.isVisible + LaunchedEffect(isGutsVisible) { gutsAlphaAnimatable.animateTo(if (isGutsVisible) 1f else 0f) } + + // Use a custom layout to measure the content even if the content is being hidden because the + // internal guts are showing. This is needed because only the content knows the size the of the + // card and the guts are set to be the same size of the content. + Layout( + content = { + CardForegroundContent( + viewModel = viewModel, + threeRows = threeRows, + fillHeight = fillHeight, + modifier = + Modifier.graphicsLayer { + compositingStrategy = CompositingStrategy.ModulateAlpha + alpha = 1f - gutsAlphaAnimatable.value + }, + ) + + CardGuts( + viewModel = viewModel.guts, + modifier = + Modifier.graphicsLayer { + compositingStrategy = CompositingStrategy.ModulateAlpha + alpha = gutsAlphaAnimatable.value + }, + ) + }, + modifier = modifier, + ) { measurables, constraints -> + check(measurables.size == 2) + val contentPlaceable = measurables[0].measure(constraints) + // Guts should always have the exact dimensions as the content, even if we don't show the + // content. + val gutsPlaceable = + measurables[1].measure( + Constraints.fixed(contentPlaceable.width, contentPlaceable.height) + ) + + layout(contentPlaceable.measuredWidth, contentPlaceable.measuredHeight) { + if (!viewModel.guts.isVisible || gutsAlphaAnimatable.isRunning) { + contentPlaceable.place(0, 0) + } + if (viewModel.guts.isVisible || gutsAlphaAnimatable.isRunning) { + gutsPlaceable.place(0, 0) + } + } + } +} + +@Composable +private fun ContentScope.CardForegroundContent( + viewModel: MediaCardViewModel, + threeRows: Boolean, + fillHeight: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .combinedClickable( + onClick = viewModel.onClick, + onLongClick = viewModel.onLongClick, + onClickLabel = viewModel.onClickLabel, + ) + .padding(16.dp) + ) { + // Always add the first/top row, regardless of presentation style. + Row(verticalAlignment = Alignment.CenterVertically) { + // Icon. + Icon( + icon = viewModel.icon, + tint = LocalAndroidColorScheme.current.primaryFixed, + modifier = Modifier.size(24.dp).clip(CircleShape), + ) + Spacer(modifier = Modifier.weight(1f)) + viewModel.outputSwitcherChips.fastForEach { chip -> + OutputSwitcherChip(viewModel = chip, modifier = Modifier.padding(start = 8.dp)) + } + } + + // If the card is taller than necessary to show all the rows, this adds spacing + // between the top row and the rows below, anchoring the next rows to the bottom + // of the card. + if (fillHeight) { + Spacer(Modifier.weight(1f)) + } + + if (threeRows) { + // Three row presentation style. + // + // Second row. + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 16.dp), + ) { + Metadata( + title = viewModel.title, + subtitle = viewModel.subtitle, + color = Color.White, + modifier = Modifier.weight(1f).padding(end = 8.dp), + ) + + AnimatedVisibility(visible = viewModel.playPauseAction.isVisible) { + PlayPauseAction( + viewModel = viewModel.playPauseAction, + buttonWidth = 48.dp, + buttonColor = LocalAndroidColorScheme.current.primaryFixed, + iconColor = LocalAndroidColorScheme.current.onPrimaryFixed, + buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp }, + ) + } + } + + // Third row. + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 24.dp), + ) { + Navigation(viewModel = viewModel.seekBar, isSeekBarVisible = true) + viewModel.additionalActions.fastForEachIndexed { index, action -> + SecondaryAction( + viewModel = action, + element = Media.Elements.additionalActionButton(index), + ) + } + } + } else { + // Two row presentation style. + // + // Bottom row. + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 36.dp), + ) { + Metadata( + title = viewModel.title, + subtitle = viewModel.subtitle, + color = Color.White, + modifier = Modifier.weight(1f).padding(end = 8.dp), + ) + + Navigation( + viewModel = viewModel.seekBar, + isSeekBarVisible = false, + modifier = Modifier.padding(end = 8.dp), + ) + + PlayPauseAction( + viewModel = viewModel.playPauseAction, + buttonWidth = 48.dp, + buttonColor = LocalAndroidColorScheme.current.primaryFixed, + iconColor = LocalAndroidColorScheme.current.onPrimaryFixed, + buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp }, + ) + } + } + } +} + +/** + * Renders a simplified version of [CardForeground] that puts everything on a single row and doesn't + * support the guts. + */ +@Composable +private fun ContentScope.CompactCardForeground( + viewModel: MediaCardViewModel, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = + modifier + .clickable(onClick = viewModel.onClick, onClickLabel = viewModel.onClickLabel) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(16.dp), + ) { + Icon( + icon = viewModel.icon, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(24.dp), + ) + + Metadata( + title = viewModel.title, + subtitle = viewModel.subtitle, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + + SecondaryAction( + viewModel = viewModel.outputSwitcherChipButton, + element = Media.Elements.OutputSwitcherButton, + iconColor = MaterialTheme.colorScheme.onSurface, + ) + + val nextAction = (viewModel.seekBar as? MediaSeekBarViewModel.Showing)?.next + if (nextAction != null) { + SecondaryAction( + viewModel = nextAction, + element = Media.Elements.NextButton, + iconColor = MaterialTheme.colorScheme.onSurface, + ) + } + + AnimatedVisibility(visible = viewModel.playPauseAction.isVisible) { + PlayPauseAction( + viewModel = viewModel.playPauseAction, + buttonWidth = 72.dp, + buttonColor = MaterialTheme.colorScheme.primaryContainer, + iconColor = MaterialTheme.colorScheme.onPrimaryContainer, + buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 24.dp }, + ) + } + } +} + +/** Renders the background of a card, loading the artwork and showing an overlay on top of it. */ +@Composable +private fun CardBackground(imageLoader: suspend () -> ImageBitmap, modifier: Modifier = Modifier) { + var image: ImageBitmap? by remember { mutableStateOf(null) } + LaunchedEffect(imageLoader) { + image = null + image = imageLoader() + } + + val gradientBaseColor = MaterialTheme.colorScheme.onSurface + Box( + modifier = + modifier.drawWithContent { + // Draw the content of the box (loaded art or placeholder). + drawContent() + + if (image != null) { + // Then draw the overlay. + drawRect( + brush = + Brush.radialGradient( + 0f to gradientBaseColor.copy(alpha = 0.65f), + 1f to gradientBaseColor.copy(alpha = 0.75f), + center = size.center, + radius = max(size.width, size.height) / 2, + ) + ) + } + } + ) { + image?.let { loadedImage -> + // Loaded art. + Image( + bitmap = loadedImage, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize(), + ) + } + ?: run { + // Placeholder. + Box(Modifier.background(MaterialTheme.colorScheme.onSurface).matchParentSize()) + } + } +} + +/** + * Renders the navigation UI (seek bar and/or previous/next buttons). + * + * If [isSeekBarVisible] is `false`, the seek bar will not be included in the layout, even if it + * would otherwise be showing based on the view-model alone. This is meant for callers to decide + * whether they'd like to show the seek bar in addition to the prev/next buttons or just show the + * buttons. + */ +@Composable +private fun ContentScope.Navigation( + viewModel: MediaSeekBarViewModel, + isSeekBarVisible: Boolean, + modifier: Modifier = Modifier, +) { + when (viewModel) { + is MediaSeekBarViewModel.Showing -> { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + viewModel.previous?.let { + SecondaryAction(viewModel = it, element = Media.Elements.PrevButton) + } + + val interactionSource = remember { MutableInteractionSource() } + val colors = + colors( + activeTrackColor = Color.White, + inactiveTrackColor = Color.White.copy(alpha = 0.3f), + thumbColor = Color.White, + ) + if (isSeekBarVisible) { + // To allow the seek bar slider to fade in and out, it's tagged as an element. + Element(key = Media.Elements.SeekBarSlider, modifier = Modifier.weight(1f)) { + Slider( + interactionSource = interactionSource, + value = viewModel.progress, + onValueChange = { progress -> viewModel.onScrubChange(progress) }, + onValueChangeFinished = { viewModel.onScrubFinished() }, + colors = colors, + thumb = { + SeekBarThumb(interactionSource = interactionSource, colors = colors) + }, + track = { sliderState -> + SeekBarTrack( + sliderState = sliderState, + isSquiggly = viewModel.isSquiggly, + colors = colors, + modifier = Modifier.fillMaxWidth(), + ) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + viewModel.next?.let { + SecondaryAction(viewModel = it, element = Media.Elements.NextButton) + } + } + } + + is MediaSeekBarViewModel.Hidden -> Unit + } +} + +/** Renders the thumb of the seek bar. */ +@Composable +private fun SeekBarThumb( + interactionSource: MutableInteractionSource, + colors: SliderColors, + modifier: Modifier = Modifier, +) { + val interactions = remember { mutableStateListOf<Interaction>() } + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> interactions.add(interaction) + is PressInteraction.Release -> interactions.remove(interaction.press) + is PressInteraction.Cancel -> interactions.remove(interaction.press) + is DragInteraction.Start -> interactions.add(interaction) + is DragInteraction.Stop -> interactions.remove(interaction.start) + is DragInteraction.Cancel -> interactions.remove(interaction.start) + } + } + } + + Spacer( + modifier + .size(width = 4.dp, height = 16.dp) + .hoverable(interactionSource = interactionSource) + .background(color = colors.thumbColor, shape = RoundedCornerShape(16.dp)) + ) +} + +/** + * Renders the track of the seek bar. + * + * If [isSquiggly] is `true`, the part to the left of the thumb will animate a squiggly line that + * oscillates up and down. The [waveLength] and [amplitude] control the geometry of the squiggle and + * the [waveSpeedDpPerSec] controls the speed by which it seems to "move" horizontally. + */ +@Composable +private fun SeekBarTrack( + sliderState: SliderState, + isSquiggly: Boolean, + colors: SliderColors, + modifier: Modifier = Modifier, + waveLength: Dp = 20.dp, + amplitude: Dp = 3.dp, + waveSpeedDpPerSec: Dp = 8.dp, +) { + // Animating the amplitude allows the squiggle to gradually grow to its full height or shrink + // back to a flat line as needed. + val animatedAmplitude by + animateDpAsState( + targetValue = if (isSquiggly) amplitude else 0.dp, + label = "SeekBarTrack.amplitude", + ) + + // This animates the horizontal movement of the squiggle. + val animatedWaveOffset = remember { Animatable(0f) } + + LaunchedEffect(isSquiggly) { + if (isSquiggly) { + animatedWaveOffset.snapTo(0f) + animatedWaveOffset.animateTo( + targetValue = 1f, + animationSpec = + infiniteRepeatable( + animation = + tween( + durationMillis = (1000 * (waveLength / waveSpeedDpPerSec)).toInt(), + easing = LinearEasing, + ), + repeatMode = RepeatMode.Restart, + ), + ) + } + } + + // Render the track. + Canvas(modifier = modifier) { + val thumbPositionPx = size.width * sliderState.value + + // The squiggly part before the thumb. + if (thumbPositionPx > 0) { + val amplitudePx = amplitude.toPx() + val animatedAmplitudePx = animatedAmplitude.toPx() + val waveLengthPx = waveLength.toPx() + + val path = + Path().apply { + val halfWaveLengthPx = waveLengthPx / 2 + val halfWaveCount = (thumbPositionPx / halfWaveLengthPx).toInt() + + repeat(halfWaveCount + 3) { index -> + // Draw a half wave (either a hill or a valley shape starting and ending on + // the horizontal center). + relativeQuadraticTo( + // The control point for the bezier curve is on top of the peak of the + // hill or the very center bottom of the valley shape. + dx1 = halfWaveLengthPx / 2, + dy1 = if (index % 2 == 0) -animatedAmplitudePx else animatedAmplitudePx, + // Advance horizontally, half a wave length at a time. + dx2 = halfWaveLengthPx, + dy2 = 0f, + ) + } + } + + // Now that the squiggle is rendered a bit past the thumb, clip off the part that passed + // the thumb. It's easier to clip the extra squiggle than to figure out the bezier curve + // for part of a hill/valley. + clipRect( + left = 0f, + top = -amplitudePx, + right = thumbPositionPx, + bottom = amplitudePx * 2, + ) { + translate(left = -waveLengthPx * animatedWaveOffset.value, top = 0f) { + // Actually render the squiggle. + drawPath( + path = path, + color = colors.activeTrackColor, + style = Stroke(width = 2.dp.toPx(), cap = StrokeCap.Round), + ) + } + } + } + + // The flat line after the thumb. + drawLine( + color = colors.inactiveTrackColor, + start = Offset(thumbPositionPx, 0f), + end = Offset(size.width, 0f), + strokeWidth = 2.dp.toPx(), + cap = StrokeCap.Round, + ) + } +} + +/** Renders the internal "guts" of a card. */ +@Composable +private fun CardGuts(viewModel: MediaCardGutsViewModel, modifier: Modifier = Modifier) { + Box( + modifier = + modifier.pointerInput(Unit) { detectLongPressGesture { viewModel.onLongClick() } } + ) { + // Settings button. + Icon( + icon = checkNotNull(viewModel.settingsButton.icon), + modifier = + Modifier.align(Alignment.TopEnd).padding(top = 16.dp, end = 16.dp).clickable { + viewModel.settingsButton.onClick() + }, + ) + + // Content. + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = + Modifier.align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(start = 16.dp, end = 32.dp, bottom = 40.dp), + ) { + Text(text = viewModel.text, color = Color.White) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + PlatformButton( + onClick = viewModel.primaryAction.onClick, + colors = ButtonDefaults.buttonColors(containerColor = Color.White), + ) { + Text( + text = checkNotNull(viewModel.primaryAction.text), + color = LocalAndroidColorScheme.current.onPrimaryFixed, + ) + } + + viewModel.secondaryAction?.let { button -> + PlatformOutlinedButton( + onClick = button.onClick, + border = BorderStroke(width = 1.dp, color = Color.White), + ) { + Text(text = checkNotNull(button.text), color = Color.White) + } + } + } + } + } +} /** Renders the metadata labels of a track. */ @Composable @@ -207,13 +852,48 @@ private fun SecondaryActionContent( iconResource = (viewModel.icon as Icon.Resource).res, contentDescription = viewModel.icon.contentDescription?.load(), colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor), + enabled = viewModel.isEnabled, modifier = modifier.size(48.dp).padding(13.dp), ) } +/** Enumerates all supported media presentation styles. */ +enum class MediaPresentationStyle { + /** The "normal" 3-row carousel look. */ + Default, + /** Similar to [Default] but not as tall (2-row carousel look). */ + Compressed, + /** A special single-row treatment that fits nicely in quick settings. */ + Compact, +} + private object Media { /** + * Scenes. + * + * The implementation uses a [SceneTransitionLayout] to smoothly animate transitions between + * different card layouts. Each card layout is identified as its own "scene" and the STL + * framework takes care of animating the layouts and their elements as the card morphs between + * scenes. + */ + object Scenes { + /** The "normal" 3-row carousel look. */ + val Default = SceneKey("default") + /** Similar to [Default] but not as tall (2-row carousel look). */ + val Compressed = SceneKey("compressed") + /** A special single-row treatment that fits nicely in quick settings. */ + val Compact = SceneKey("compact") + } + + /** Definitions of how scene changes are transition-animated. */ + val Transitions = transitions { + from(Scenes.Default, to = Scenes.Compact) {} + from(Scenes.Default, to = Scenes.Compressed) { fade(Elements.SeekBarSlider) } + from(Scenes.Compact, to = Scenes.Compressed) { fade(Elements.SeekBarSlider) } + } + + /** * Element keys. * * Composables that are wrapped in [ContentScope.Element] with one of these as their `key` @@ -226,5 +906,21 @@ private object Media { object Elements { val PlayPauseButton = ElementKey("play_pause") val Metadata = ElementKey("metadata") + val PrevButton = ElementKey("prev") + val NextButton = ElementKey("next") + val SeekBarSlider = ElementKey("seek_bar_slider") + val OutputSwitcherButton = ElementKey("output_switcher") + + fun additionalActionButton(index: Int): ElementKey { + return ElementKey("additional_action_$index") + } + } +} + +private fun MediaPresentationStyle.toScene(): SceneKey { + return when (this) { + MediaPresentationStyle.Default -> Media.Scenes.Default + MediaPresentationStyle.Compressed -> Media.Scenes.Compressed + MediaPresentationStyle.Compact -> Media.Scenes.Compact } } diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardGutsViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardGutsViewModel.kt new file mode 100644 index 000000000000..61e3bdced5f8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardGutsViewModel.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.media.remedia.ui.viewmodel + +data class MediaCardGutsViewModel( + val isVisible: Boolean, + val text: String, + val primaryAction: MediaGutsButtonViewModel, + val secondaryAction: MediaGutsButtonViewModel? = null, + val settingsButton: MediaGutsSettingsButtonViewModel, + val onLongClick: () -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt new file mode 100644 index 000000000000..ecd6e6d094d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt @@ -0,0 +1,64 @@ +/* + * 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.media.remedia.ui.viewmodel + +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.ImageBitmap +import com.android.systemui.common.shared.model.Icon + +/** Models UI state for a media card. */ +@Stable +interface MediaCardViewModel { + /** + * Identifier. Must be unique across all media cards currently shown, to help the horizontal + * pager in the UI. + */ + val key: Any + + val icon: Icon + + /** + * A callback to load the artwork for the media shown on this card. This callback will be + * invoked on the main thread, it's up to the implementation to move the loading off the main + * thread. + */ + val artLoader: suspend () -> ImageBitmap + + val title: String + + val subtitle: String + + val playPauseAction: MediaPlayPauseActionViewModel + + val seekBar: MediaSeekBarViewModel + + val additionalActions: List<MediaSecondaryActionViewModel> + + val guts: MediaCardGutsViewModel + + val outputSwitcherChips: List<MediaOutputSwitcherChipViewModel> + + /** Simple icon-only version of the output switcher for use in compact UIs. */ + val outputSwitcherChipButton: MediaSecondaryActionViewModel + + val onClick: () -> Unit + + /** Accessibility string for the click action of the card. */ + val onClickLabel: String? + + val onLongClick: () -> Unit +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/dagger/qualifiers/Tracing.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaGutsButtonViewModel.kt index 9b7cd704aa2f..6ce0a014455f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/dagger/qualifiers/Tracing.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaGutsButtonViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.dagger.qualifiers -import javax.inject.Qualifier +package com.android.systemui.media.remedia.ui.viewmodel -@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Tracing +data class MediaGutsButtonViewModel(val text: String, val onClick: () -> Unit) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaGutsSettingsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaGutsSettingsButtonViewModel.kt new file mode 100644 index 000000000000..fabfe0e147c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaGutsSettingsButtonViewModel.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.media.remedia.ui.viewmodel + +import com.android.systemui.common.shared.model.Icon + +data class MediaGutsSettingsButtonViewModel(val icon: Icon, val onClick: () -> Unit) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt index 2d7765d861a7..a4806800a7b1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt @@ -19,4 +19,8 @@ package com.android.systemui.media.remedia.ui.viewmodel import com.android.systemui.common.shared.model.Icon /** Models UI state for a secondary action button within media controls. */ -data class MediaSecondaryActionViewModel(val icon: Icon, val onClick: () -> Unit) +data class MediaSecondaryActionViewModel( + val icon: Icon, + val isEnabled: Boolean, + val onClick: () -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt new file mode 100644 index 000000000000..f1ced6bf908d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt @@ -0,0 +1,57 @@ +/* + * 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.media.remedia.ui.viewmodel + +import androidx.annotation.FloatRange + +/** Models UI state for the seek bar. */ +sealed interface MediaSeekBarViewModel { + + /** The seek bar should be showing. */ + data class Showing( + /** The progress to show on the seek bar, between `0` and `1`. */ + @FloatRange(from = 0.0, to = 1.0) val progress: Float, + /** The previous button; or `null` if it should be absent in the UI. */ + val previous: MediaSecondaryActionViewModel?, + /** The next button; or `null` if it should be absent in the UI. */ + val next: MediaSecondaryActionViewModel?, + /** + * Whether the portion of the seek bar track before the thumb should show the squiggle + * animation. + */ + val isSquiggly: Boolean, + /** + * Whether the UI should show as "scrubbing" because the user is actively moving the thumb + * of the seek bar. + */ + val isScrubbing: Boolean, + /** + * A callback to invoke while the user is "scrubbing" (e.g. actively moving the thumb of the + * seek bar). The position/progress of the actual track should not be changed during this + * time. + */ + val onScrubChange: (progress: Float) -> Unit, + /** + * A callback to invoke once the user finishes "scrubbing" (e.g. stopped moving the thumb of + * the seek bar). The position/progress should be committed. + */ + val onScrubFinished: () -> Unit, + ) : MediaSeekBarViewModel + + /** The seek bar should be hidden. */ + data object Hidden : MediaSeekBarViewModel +} diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt b/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt index f29b15730986..aaed606f8fb2 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUIStateChange.kt @@ -16,6 +16,8 @@ package com.android.systemui.model +import com.android.systemui.shared.system.QuickStepContract.getSystemUiStateString + /** * Represents a set of state changes. A bit can either be set to `true` or `false`. * @@ -43,13 +45,16 @@ class StateChange { fun hasChanges() = flagsToSet != 0L || flagsToClear != 0L - /** Applies all changed flags to [sysUiState]. */ + /** + * Applies all changed flags to [sysUiState]. + * + * Note this doesn't call [SysUiState.commitUpdate]. + */ fun applyTo(sysUiState: SysUiState) { iterateBits(flagsToSet or flagsToClear) { bit -> val isBitSetInNewState = flagsToSet and bit != 0L sysUiState.setFlag(bit, isBitSetInNewState) } - sysUiState.commitUpdate() } fun applyTo(sysUiState: Long): Long { @@ -69,14 +74,25 @@ class StateChange { } } - /** Clears all the flags changed in a [sysUiState] */ - fun clearAllChangedFlagsIn(sysUiState: SysUiState) { + /** + * Clears all the flags changed in a [sysUiState]. + * + * Note this doesn't call [SysUiState.commitUpdate]. + */ + fun clearFrom(sysUiState: SysUiState) { iterateBits(flagsToSet or flagsToClear) { bit -> sysUiState.setFlag(bit, false) } - sysUiState.commitUpdate() } fun clear() { flagsToSet = 0 flagsToClear = 0 } + + override fun toString(): String { + return """StateChange(flagsToSet=${getSystemUiStateString(flagsToSet)}, flagsToClear=${ + getSystemUiStateString( + flagsToClear + ) + })""" + } } diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUIStateOverride.kt b/packages/SystemUI/src/com/android/systemui/model/SysUIStateOverride.kt new file mode 100644 index 000000000000..89e06e226997 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/model/SysUIStateOverride.kt @@ -0,0 +1,83 @@ +/* + * 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.model + +import android.view.Display +import com.android.systemui.dump.DumpManager +import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * This class is used to provide per-display overrides for only certain flags. + * + * While some of the [SystemUiStateFlags] are per display (e.g. shade expansion, dialog visible), + * some of them are device specific (e.g. whether it's awake or not). A [SysUIStateOverride] is + * created for each display that is not [Display.DEFAULT_DISPLAY], and if some flags are set on it, + * they will override whatever the default display state had in those. + */ +class SysUIStateOverride +@AssistedInject +constructor( + @Assisted override val displayId: Int, + private val sceneContainerPlugin: SceneContainerPlugin?, + dumpManager: DumpManager, + private val defaultDisplayState: SysUiState, + private val stateDispatcher: SysUIStateDispatcher, +) : SysUiStateImpl(displayId, sceneContainerPlugin, dumpManager, stateDispatcher) { + + private val override = StateChange() + private var lastSentFlags = defaultDisplayState.flags + + private val defaultFlagsChangedCallback = { _: Long, otherDisplayId: Int -> + if (otherDisplayId == Display.DEFAULT_DISPLAY) { + commitUpdate() + } + } + + override fun start() { + super.start() + stateDispatcher.registerListener(defaultFlagsChangedCallback) + } + + override fun destroy() { + super.destroy() + stateDispatcher.unregisterListener(defaultFlagsChangedCallback) + } + + override fun commitUpdate() { + if (flags != lastSentFlags) { + stateDispatcher.dispatchSysUIStateChange(flags, displayId) + lastSentFlags = flags + } + } + + override val flags: Long + get() = override.applyTo(defaultDisplayState.flags) + + override fun setFlag(@SystemUiStateFlags flag: Long, enabled: Boolean): SysUiState { + val toSet = flagWithOptionalOverrides(flag, enabled, displayId, sceneContainerPlugin) + override.setFlag(flag, toSet) + return this + } + + @AssistedFactory + interface Factory { + fun create(displayId: Int): SysUIStateOverride + } +} diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt index 53105b2c0f6a..e99ee7ddb919 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SysUiState.kt @@ -16,6 +16,7 @@ package com.android.systemui.model import android.util.Log +import android.view.Display import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.display.data.repository.PerDisplayInstanceProviderWithTeardown @@ -74,6 +75,12 @@ interface SysUiState : Dumpable { */ fun destroy() + /** Initializes the state after construction. */ + fun start() + + /** The display ID this instances is associated with */ + val displayId: Int + companion object { const val DEBUG: Boolean = false } @@ -81,18 +88,19 @@ interface SysUiState : Dumpable { private const val TAG = "SysUIState" -class SysUiStateImpl +open class SysUiStateImpl @AssistedInject constructor( - @Assisted private val displayId: Int, + @Assisted override val displayId: Int, private val sceneContainerPlugin: SceneContainerPlugin?, private val dumpManager: DumpManager, private val stateDispatcher: SysUIStateDispatcher, ) : SysUiState { - private val debugName = "SysUiStateImpl-ForDisplay=$displayId" + private val debugName + get() = "SysUiStateImpl-ForDisplay=$displayId" - init { + override fun start() { dumpManager.registerNormalDumpable(debugName, this) } @@ -219,10 +227,19 @@ fun flagWithOptionalOverrides( /** Creates and destroy instances of [SysUiState] */ @SysUISingleton -class SysUIStateInstanceProvider @Inject constructor(private val factory: SysUiStateImpl.Factory) : - PerDisplayInstanceProviderWithTeardown<SysUiState> { +class SysUIStateInstanceProvider +@Inject +constructor( + private val factory: SysUiStateImpl.Factory, + private val overrideFactory: SysUIStateOverride.Factory, +) : PerDisplayInstanceProviderWithTeardown<SysUiState> { override fun createInstance(displayId: Int): SysUiState { - return factory.create(displayId) + return if (displayId == Display.DEFAULT_DISPLAY) { + factory.create(displayId) + } else { + overrideFactory.create(displayId) + } + .apply { start() } } override fun destroyInstance(instance: SysUiState) { diff --git a/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt b/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt index a344a5cce6ba..3f804f768d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt +++ b/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt @@ -42,7 +42,9 @@ object ModesUi { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, Flags.FLAG_MODES_UI) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, Flags.FLAG_MODES_UI) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java index 5fa0095d2329..50d0a459da66 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java @@ -18,7 +18,6 @@ package com.android.systemui.navigationbar; import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG; import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen; -import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification; import static com.android.wm.shell.Flags.enableTaskbarOnPhones; @@ -37,6 +36,7 @@ import android.view.Display; import android.view.IWindowManager; import android.view.View; import android.view.WindowManagerGlobal; +import android.window.DesktopExperienceFlags; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -285,7 +285,7 @@ public class NavigationBarControllerImpl implements @Override public void onDisplayAddSystemDecorations(int displayId) { - if (enableDisplayContentModeManagement()) { + if (DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) { mHasNavBar.put(displayId, true); } Display display = mDisplayManager.getDisplay(displayId); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index c4d847f18269..efed260b4c99 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -44,7 +44,6 @@ import android.app.StatusBarManager.WindowVisibleState; import android.content.Context; import android.graphics.Rect; import android.hardware.display.DisplayManager; -import android.inputmethodservice.InputMethodService; import android.inputmethodservice.InputMethodService.BackDispositionMode; import android.inputmethodservice.InputMethodService.ImeWindowVisibility; import android.os.Handler; @@ -503,9 +502,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, @Override public void setImeWindowStatus(int displayId, @ImeWindowVisibility int vis, @BackDispositionMode int backDisposition, boolean showImeSwitcher) { - // Count imperceptible changes as visible so we transition taskbar out quickly. - final boolean isImeVisible = mNavBarHelper.isImeVisible(vis) - || (vis & InputMethodService.IME_VISIBLE_IMPERCEPTIBLE) != 0; + final boolean isImeVisible = mNavBarHelper.isImeVisible(vis); final int flags = Utilities.updateNavbarFlagsFromIme(mNavbarFlags, backDisposition, isImeVisible, showImeSwitcher); if (flags == mNavbarFlags) { 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 5e7e0c97a147..f1f5b267f9c1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -63,6 +63,7 @@ import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.approachLayout +import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot @@ -250,10 +251,23 @@ constructor( private fun Content() { PlatformTheme(isDarkTheme = true) { ProvideShortcutHelperIndication(interactionsConfig = interactionsConfig()) { - if (viewModel.isQsVisibleAndAnyShadeExpanded) { + // TODO(b/389985793): Make sure that there is no coroutine work or recompositions + // happening when alwaysCompose is true but isQsVisibleAndAnyShadeExpanded is false. + if (alwaysCompose || viewModel.isQsVisibleAndAnyShadeExpanded) { Box( modifier = - Modifier.graphicsLayer { alpha = viewModel.viewAlpha } + Modifier.thenIf(alwaysCompose) { + Modifier.layout { measurable, constraints -> + measurable.measure(constraints).run { + layout(width, height) { + if (viewModel.isQsVisibleAndAnyShadeExpanded) { + place(0, 0) + } + } + } + } + } + .graphicsLayer { alpha = viewModel.viewAlpha } .thenIf(notificationScrimClippingParams.isEnabled) { Modifier.notificationScrimClip { notificationScrimClippingParams.params @@ -331,12 +345,12 @@ constructor( } SceneTransitionLayout(state = sceneState, modifier = Modifier.fillMaxSize()) { - scene(QuickSettings) { + scene(QuickSettings, alwaysCompose = alwaysCompose) { LaunchedEffect(Unit) { viewModel.onQSOpen() } Element(QuickSettings.rootElementKey, Modifier) { QuickSettingsElement() } } - scene(QuickQuickSettings) { + scene(QuickQuickSettings, alwaysCompose = alwaysCompose) { LaunchedEffect(Unit) { viewModel.onQQSOpen() } // Cannot pass the element modifier in because the top element has a `testTag` // and this would overwrite it. @@ -626,7 +640,20 @@ constructor( ) { val Tiles = @Composable { - QuickQuickSettings(viewModel = viewModel.quickQuickSettingsViewModel) + QuickQuickSettings( + viewModel = viewModel.quickQuickSettingsViewModel, + listening = { + /* + * When always compose is false, this will always be true, and we'll be + * listening whenever this is composed. + * When always compose is true, we listen if we are visible and not + * fully expanded + */ + !alwaysCompose || + (viewModel.isQsVisibleAndAnyShadeExpanded && + viewModel.expansionState.progress < 1f) + }, + ) } val Media = @Composable { @@ -716,7 +743,13 @@ constructor( BrightnessSliderContainer( viewModel = containerViewModel.brightnessSliderViewModel, modifier = - Modifier.systemGestureExclusionInShade().fillMaxWidth(), + Modifier.systemGestureExclusionInShade( + enabled = { + layoutState.transitionState is + TransitionState.Idle + } + ) + .fillMaxWidth(), ) } val TileGrid = @@ -726,6 +759,18 @@ constructor( TileGrid( viewModel = containerViewModel.tileGridViewModel, modifier = Modifier.fillMaxWidth(), + listening = { + /* + * When always compose is false, this will always be true, + * and we'll be listening whenever this is composed. + * When always compose is true, we look a the second + * condition and we'll listen if QS is visible AND we are + * not fully collapsed. + */ + !alwaysCompose || + (viewModel.isQsVisibleAndAnyShadeExpanded && + viewModel.expansionState.progress > 0f) + }, ) } } @@ -830,6 +875,7 @@ constructor( println("qqsPositionOnScreen", rect) } println("QQS visible", qqsVisible.value) + println("Always composed", alwaysCompose) if (::viewModel.isInitialized) { printSection("View Model") { viewModel.dump(this@run, args) } } @@ -1177,3 +1223,6 @@ private fun interactionsConfig() = // we are OK using this as our content is clipped and all corner radius are larger than this surfaceCornerRadius = 28.dp, ) + +private inline val alwaysCompose + get() = Flags.alwaysComposeQsUiFragment() diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt index 2eba36a25ae7..6865e0ee79f6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt @@ -81,7 +81,9 @@ object QsDetailedView { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** Returns a developer-readable string that describes the current requirement list. */ @JvmStatic diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt index 2670787909b4..c45fa973f973 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.footer.ui.viewmodel import android.annotation.AttrRes import android.annotation.ColorInt +import androidx.compose.runtime.Stable import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon @@ -25,6 +26,7 @@ import com.android.systemui.common.shared.model.Icon * A ViewModel for a simple footer actions button. This is used for the user switcher, settings and * power buttons. */ +@Stable data class FooterActionsButtonViewModel( val id: Int, val icon: Icon, diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index 9546e355dca2..aea280cc9f91 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -75,6 +75,7 @@ class FooterActionsViewModel( /** The model for the power button. */ val power: Flow<FooterActionsButtonViewModel?>, + val initialPower: () -> FooterActionsButtonViewModel?, /** * Observe the device monitoring dialog requests and show the dialog accordingly. This function @@ -181,7 +182,7 @@ class FooterActionsViewModel( fun createFooterActionsViewModel( @ShadeDisplayAware appContext: Context, footerActionsInteractor: FooterActionsInteractor, - shadeMode: Flow<ShadeMode>, + shadeMode: StateFlow<ShadeMode>, falsingManager: FalsingManager, globalActionsDialogLite: GlobalActionsDialogLite, activityStarter: ActivityStarter, @@ -291,6 +292,12 @@ fun createFooterActionsViewModel( settings = settings, power = power, observeDeviceMonitoringDialogRequests = ::observeDeviceMonitoringDialogRequests, + initialPower = + if (showPowerButton) { + { powerButtonViewModel(qsThemedContext, ::onPowerButtonClicked, shadeMode.value) } + } else { + { null } + }, ) } @@ -411,20 +418,28 @@ fun powerButtonViewModel( shadeMode: Flow<ShadeMode>, ): Flow<FooterActionsButtonViewModel?> { return shadeMode.map { mode -> - val isDualShade = mode is ShadeMode.Dual - FooterActionsButtonViewModel( - id = R.id.pm_lite, - Icon.Resource( - android.R.drawable.ic_lock_power_off, - ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu), - ), - iconTint = - Utils.getColorAttrDefaultColor( - qsThemedContext, - if (isDualShade) R.attr.onShadeInactiveVariant else R.attr.onShadeActive, - ), - backgroundColor = if (isDualShade) R.attr.shadeInactive else R.attr.shadeActive, - onPowerButtonClicked, - ) + powerButtonViewModel(qsThemedContext, onPowerButtonClicked, mode) } } + +fun powerButtonViewModel( + qsThemedContext: Context, + onPowerButtonClicked: (Expandable) -> Unit, + shadeMode: ShadeMode, +): FooterActionsButtonViewModel { + val isDualShade = shadeMode is ShadeMode.Dual + return FooterActionsButtonViewModel( + id = R.id.pm_lite, + Icon.Resource( + android.R.drawable.ic_lock_power_off, + ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu), + ), + iconTint = + Utils.getColorAttrDefaultColor( + qsThemedContext, + if (isDualShade) R.attr.onShadeInactiveVariant else R.attr.onShadeActive, + ), + backgroundColor = if (isDualShade) R.attr.shadeInactive else R.attr.shadeActive, + onPowerButtonClicked, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt index 185ea93387a3..1fb884de620f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt @@ -27,7 +27,17 @@ import com.android.systemui.qs.pipeline.shared.TileSpec /** A layout of tiles, indicating how they should be composed when showing in QS or in edit mode. */ interface GridLayout { - @Composable fun ContentScope.TileGrid(tiles: List<TileViewModel>, modifier: Modifier) + + /** + * [listening] can be used to compose the grid but limit when tiles should be listening. It + * should be a function tracking a snapshot state. + */ + @Composable + fun ContentScope.TileGrid( + tiles: List<TileViewModel>, + modifier: Modifier, + listening: () -> Boolean, + ) @Composable fun EditTileGrid( diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index a07120629d2b..865ae9a37d87 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow @@ -43,6 +42,7 @@ import androidx.compose.ui.res.integerResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ContentScope import com.android.compose.modifiers.padding +import com.android.systemui.common.ui.compose.PagerDots import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.development.ui.compose.BuildNumber import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel @@ -65,17 +65,16 @@ constructor( @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout, ) : GridLayout by delegateGridLayout { @Composable - override fun ContentScope.TileGrid(tiles: List<TileViewModel>, modifier: Modifier) { + override fun ContentScope.TileGrid( + tiles: List<TileViewModel>, + modifier: Modifier, + listening: () -> Boolean, + ) { val viewModel = rememberViewModel(traceName = "PaginatedGridLayout-TileGrid") { viewModelFactory.create() } - DisposableEffect(tiles) { - val token = Any() - tiles.forEach { it.startListening(token) } - onDispose { tiles.forEach { it.stopListening(token) } } - } val columns = viewModel.columns val rows = integerResource(R.integer.quick_settings_paginated_grid_num_rows) @@ -122,7 +121,7 @@ constructor( ) { val page = pages[it] - with(delegateGridLayout) { TileGrid(tiles = page, modifier = Modifier) } + with(delegateGridLayout) { TileGrid(tiles = page, modifier = Modifier, listening) } } FooterBar( buildNumberViewModelFactory = viewModel.buildNumberViewModelFactory, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index cdc03bb9be35..d20b360756d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -18,7 +18,6 @@ package com.android.systemui.qs.panels.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -41,6 +40,7 @@ import com.android.systemui.res.R fun ContentScope.QuickQuickSettings( viewModel: QuickQuickSettingsViewModel, modifier: Modifier = Modifier, + listening: () -> Boolean, ) { val sizedTiles = viewModel.tileViewModels @@ -51,11 +51,6 @@ fun ContentScope.QuickQuickSettings( val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } - DisposableEffect(tiles) { - val token = Any() - tiles.forEach { it.startListening(token) } - onDispose { tiles.forEach { it.stopListening(token) } } - } val columns = viewModel.columns Box(modifier = modifier) { GridAnchor() @@ -91,4 +86,6 @@ fun ContentScope.QuickQuickSettings( } } } + + TileListener(tiles, listening) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt index fd10f917106b..1858825f91ef 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt @@ -22,9 +22,13 @@ import com.android.compose.animation.scene.ContentScope import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel @Composable -fun ContentScope.TileGrid(viewModel: TileGridViewModel, modifier: Modifier = Modifier) { +fun ContentScope.TileGrid( + viewModel: TileGridViewModel, + modifier: Modifier = Modifier, + listening: () -> Boolean = { true }, +) { val gridLayout = viewModel.gridLayout val tiles = viewModel.tileViewModels - with(gridLayout) { TileGrid(tiles, modifier) } + with(gridLayout) { TileGrid(tiles, modifier, listening) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileListener.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileListener.kt new file mode 100644 index 000000000000..6fb8f1bb79a1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileListener.kt @@ -0,0 +1,47 @@ +/* + * 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.panels.ui.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel + +/** + * Use to have tiles start/stop listening when this is composed. Additionally, the listening state + * will be gated by [listeningEnabled]. + */ +@Composable +fun TileListener(tiles: List<TileViewModel>, listeningEnabled: () -> Boolean) { + LaunchedEffect(tiles) { + val token = Any() + try { + snapshotFlow { listeningEnabled() } + .collect { listening -> + tiles.forEach { + if (listening) { + it.startListening(token) + } else { + it.stopListening(token) + } + } + } + } finally { + tiles.forEach { it.stopListening(token) } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 85658bb71c8b..50012abc69d6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -26,12 +26,14 @@ import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -39,6 +41,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -49,8 +52,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorProducer +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -60,7 +71,7 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.semantics.toggleableState -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.modifiers.size @@ -73,6 +84,9 @@ import com.android.systemui.common.ui.compose.load import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.SideIconHeight import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.SideIconWidth +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TILE_INITIAL_DELAY_MILLIS +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TILE_MARQUEE_ITERATIONS +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileLabelBlurWidth import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState import com.android.systemui.qs.ui.compose.borderOnFocus @@ -104,30 +118,31 @@ fun LargeTileContent( val focusBorderColor = MaterialTheme.colorScheme.secondary Box( modifier = - Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) { - Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd) - .clip(iconShape) - .verticalSquish(squishiness) - .drawBehind { drawRect(animatedBackgroundColor) } - .combinedClickable( - onClick = toggleClick!!, - onLongClick = onLongClick, - onLongClickLabel = longPressLabel, - hapticFeedbackEnabled = !Flags.msdlFeedback(), - ) - .thenIf(accessibilityUiState != null) { - Modifier.semantics { - accessibilityUiState as AccessibilityUiState - contentDescription = accessibilityUiState.contentDescription - stateDescription = accessibilityUiState.stateDescription - accessibilityUiState.toggleableState?.let { - toggleableState = it + Modifier.size(CommonTileDefaults.ToggleTargetSize) + .clip(iconShape) + .verticalSquish(squishiness) + .drawBehind { drawRect(animatedBackgroundColor) } + .thenIf(toggleClick != null) { + Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd) + .combinedClickable( + onClick = toggleClick!!, + onLongClick = onLongClick, + onLongClickLabel = longPressLabel, + hapticFeedbackEnabled = !Flags.msdlFeedback(), + ) + .thenIf(accessibilityUiState != null) { + Modifier.semantics { + accessibilityUiState as AccessibilityUiState + contentDescription = accessibilityUiState.contentDescription + stateDescription = accessibilityUiState.stateDescription + accessibilityUiState.toggleableState?.let { + toggleableState = it + } + role = Role.Switch } - role = Role.Switch - } - .sysuiResTag(TEST_TAG_TOGGLE) - } - } + .sysuiResTag(TEST_TAG_TOGGLE) + } + } ) { SmallTileContent( iconProvider = iconProvider, @@ -167,18 +182,15 @@ fun LargeTileLabels( val animatedSecondaryLabelColor by animateColorAsState(colors.secondaryLabel, label = "QSTileSecondaryLabelColor") Column(verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxHeight()) { - BasicText( - label, + TileLabel( + text = label, style = MaterialTheme.typography.labelLarge, color = { animatedLabelColor }, - maxLines = 1, - overflow = TextOverflow.Ellipsis, ) if (!TextUtils.isEmpty(secondaryLabel)) { - BasicText( + TileLabel( secondaryLabel ?: "", color = { animatedSecondaryLabelColor }, - maxLines = 1, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.thenIf( @@ -194,9 +206,9 @@ fun LargeTileLabels( @Composable fun SmallTileContent( - modifier: Modifier = Modifier, iconProvider: Context.() -> Icon, color: Color, + modifier: Modifier = Modifier, size: () -> Dp = { CommonTileDefaults.IconSize }, animateToEnd: Boolean = false, ) { @@ -212,31 +224,39 @@ fun SmallTileContent( } } if (loadedDrawable is Animatable) { + // Skip initial animation, icons should animate only as the state change + // and not when first composed + var shouldSkipInitialAnimation by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { shouldSkipInitialAnimation = animateToEnd } + val painter = when (icon) { is Icon.Resource -> { val image = AnimatedImageVector.animatedVectorResource(id = icon.res) key(icon) { - if (animateToEnd) { - rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true) - } else { - var atEnd by remember(icon.res) { mutableStateOf(false) } - LaunchedEffect(key1 = icon.res) { atEnd = true } - rememberAnimatedVectorPainter( - animatedImageVector = image, - atEnd = atEnd, - ) - } + var atEnd by remember(icon) { mutableStateOf(shouldSkipInitialAnimation) } + LaunchedEffect(key1 = icon.res) { atEnd = true } + + rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd) } } is Icon.Loaded -> { - LaunchedEffect(loadedDrawable) { + val painter = rememberDrawablePainter(loadedDrawable) + + // rememberDrawablePainter automatically starts the animation. Using + // SideEffect here to immediately stop it if needed + DisposableEffect(painter) { if (loadedDrawable is AnimatedVectorDrawable) { loadedDrawable.forceAnimationOnUI() } + if (shouldSkipInitialAnimation) { + loadedDrawable.stop() + } + onDispose {} } - rememberDrawablePainter(loadedDrawable) + + painter } } @@ -251,6 +271,45 @@ fun SmallTileContent( } } +@Composable +private fun TileLabel( + text: String, + color: ColorProducer, + style: TextStyle, + modifier: Modifier = Modifier, +) { + BasicText( + text = text, + color = color, + style = style, + maxLines = 1, + modifier = + modifier + .fillMaxWidth() + .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } + .drawWithContent { + drawContent() + // Draw a blur over the end of the text + val edgeWidthPx = TileLabelBlurWidth.toPx() + drawRect( + topLeft = Offset(size.width - edgeWidthPx, 0f), + size = Size(edgeWidthPx, size.height), + brush = + Brush.horizontalGradient( + colors = listOf(Color.Transparent, Color.Black), + startX = size.width, + endX = size.width - edgeWidthPx, + ), + blendMode = BlendMode.DstIn, + ) + } + .basicMarquee( + iterations = TILE_MARQUEE_ITERATIONS, + initialDelayMillis = TILE_INITIAL_DELAY_MILLIS, + ), + ) +} + object CommonTileDefaults { val IconSize = 32.dp val LargeTileIconSize = 28.dp @@ -258,9 +317,13 @@ object CommonTileDefaults { val SideIconHeight = 20.dp val ToggleTargetSize = 56.dp val TileHeight = 72.dp - val TilePadding = 8.dp + val TileStartPadding = 8.dp + val TileEndPadding = 16.dp val TileArrangementPadding = 6.dp val InactiveCornerRadius = 50.dp + val TileLabelBlurWidth = 32.dp + const val TILE_MARQUEE_ITERATIONS = 1 + const val TILE_INITIAL_DELAY_MILLIS = 2000 @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index ddadb8879f07..69b967a68c3c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -22,6 +22,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -78,6 +79,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -140,6 +142,7 @@ import com.android.systemui.qs.panels.ui.compose.selection.TileState import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState import com.android.systemui.qs.panels.ui.compose.selection.selectableTile +import com.android.systemui.qs.panels.ui.model.AvailableTileGridCell import com.android.systemui.qs.panels.ui.model.GridCell import com.android.systemui.qs.panels.ui.model.SpacerGridCell import com.android.systemui.qs.panels.ui.model.TileGridCell @@ -290,8 +293,19 @@ fun DefaultEditTileGrid( Text(text = stringResource(id = R.string.drag_to_add_tiles)) } + val availableTiles = remember { + mutableStateListOf<AvailableTileGridCell>().apply { + addAll(toAvailableTiles(listState.tiles, otherTiles)) + } + } + LaunchedEffect(listState.tiles, otherTiles) { + availableTiles.apply { + clear() + addAll(toAvailableTiles(listState.tiles, otherTiles)) + } + } AvailableTileGrid( - otherTiles, + availableTiles, selectionState, columns, onAddTile, @@ -444,7 +458,7 @@ private fun CurrentTilesGrid( @Composable private fun AvailableTileGrid( - tiles: List<SizedTile<EditTileViewModel>>, + tiles: List<AvailableTileGridCell>, selectionState: MutableSelectionState, columns: Int, onAddTile: (TileSpec) -> Unit, @@ -453,7 +467,7 @@ private fun AvailableTileGrid( // Available tiles aren't visible during drag and drop, so the row/col isn't needed val groupedTiles = remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) { - groupAndSort(tiles.fastMap { TileGridCell(it, 0, 0) }) + groupAndSort(tiles) } val labelColors = EditModeTileDefaults.editTileColors() @@ -478,11 +492,10 @@ private fun AvailableTileGrid( horizontalArrangement = spacedBy(TileArrangementPadding), modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max), ) { - row.forEachIndexed { index, tileGridCell -> - key(tileGridCell.tile.tileSpec) { + row.forEach { tileGridCell -> + key(tileGridCell.key) { AvailableTileGridCell( cell = tileGridCell, - index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, onAddTile = onAddTile, @@ -505,10 +518,7 @@ fun gridHeight(rows: Int, tileHeight: Dp, tilePadding: Dp, gridPadding: Dp): Dp } private fun GridCell.key(index: Int): Any { - return when (this) { - is TileGridCell -> key - is SpacerGridCell -> index - } + return if (this is TileGridCell) key else index } /** @@ -687,41 +697,44 @@ private fun TileGridCell( @Composable private fun AvailableTileGridCell( - cell: TileGridCell, - index: Int, + cell: AvailableTileGridCell, dragAndDropState: DragAndDropState, selectionState: MutableSelectionState, onAddTile: (TileSpec) -> Unit, modifier: Modifier = Modifier, ) { - val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action) - val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1) + val stateDescription: String? = + if (cell.isAvailable) null + else stringResource(R.string.accessibility_qs_edit_tile_already_added) + + val alpha by animateFloatAsState(if (cell.isAvailable) 1f else .38f) val colors = EditModeTileDefaults.editTileColors() - val onClick = { - onAddTile(cell.tile.tileSpec) - selectionState.select(cell.tile.tileSpec) - } // Displays the tile as an icon tile with the label underneath Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top), - modifier = modifier, + verticalArrangement = spacedBy(CommonTileDefaults.TileStartPadding, Alignment.Top), + modifier = + modifier + .graphicsLayer { this.alpha = alpha } + .semantics(mergeDescendants = true) { + stateDescription?.let { this.stateDescription = it } + }, ) { Box(Modifier.fillMaxWidth().height(TileHeight)) { - Box( - Modifier.fillMaxSize() - .clickable(onClick = onClick, onClickLabel = onClickActionName) - .semantics(mergeDescendants = true) { this.stateDescription = stateDescription } - .dragAndDropTileSource( + val draggableModifier = + if (cell.isAvailable) { + Modifier.dragAndDropTileSource( SizedTileImpl(cell.tile, cell.width), dragAndDropState, DragType.Add, ) { selectionState.unSelect() } - .tileBackground(colors.background) - ) { + } else { + Modifier + } + Box(draggableModifier.fillMaxSize().tileBackground(colors.background)) { // Icon SmallTileContent( iconProvider = { cell.tile.icon }, @@ -733,9 +746,13 @@ private fun AvailableTileGridCell( StaticTileBadge( icon = Icons.Default.Add, - contentDescription = onClickActionName, - onClick = onClick, - ) + contentDescription = + stringResource(id = R.string.accessibility_qs_edit_tile_add_action), + enabled = cell.isAvailable, + ) { + onAddTile(cell.tile.tileSpec) + selectionState.select(cell.tile.tileSpec) + } } Box(Modifier.fillMaxSize()) { Text( @@ -796,7 +813,7 @@ fun EditTile( placeable.place(startPadding.roundToInt(), 0) } } - .tilePadding(), + .largeTilePadding(), ) { // Icon Box(Modifier.size(ToggleTargetSize)) { @@ -819,9 +836,18 @@ fun EditTile( } } +private fun toAvailableTiles( + currentTiles: List<GridCell>, + otherTiles: List<SizedTile<EditTileViewModel>>, +): List<AvailableTileGridCell> { + return currentTiles.filterIsInstance<TileGridCell>().fastMap { + AvailableTileGridCell(it.tile, isAvailable = false) + } + otherTiles.fastMap { AvailableTileGridCell(it.tile) } +} + private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float { return (containerSize - ToggleTargetSize.roundToPx()) / 2f - - CommonTileDefaults.TilePadding.toPx() + CommonTileDefaults.TileStartPadding.toPx() } private fun Modifier.tileBackground(color: Color): Modifier { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 0503049382a8..6236ada46374 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -17,7 +17,6 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -34,6 +33,7 @@ import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout +import com.android.systemui.qs.panels.ui.compose.TileListener import com.android.systemui.qs.panels.ui.compose.bounceableInfo import com.android.systemui.qs.panels.ui.compose.rememberEditListState import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel @@ -59,12 +59,11 @@ constructor( ) : PaginatableGridLayout { @Composable - override fun ContentScope.TileGrid(tiles: List<TileViewModel>, modifier: Modifier) { - DisposableEffect(tiles) { - val token = Any() - tiles.forEach { it.startListening(token) } - onDispose { tiles.forEach { it.stopListening(token) } } - } + override fun ContentScope.TileGrid( + tiles: List<TileViewModel>, + modifier: Modifier, + listening: () -> Boolean, + ) { val viewModel = rememberViewModel(traceName = "InfiniteGridLayout.TileGrid") { viewModelFactory.create() @@ -116,6 +115,8 @@ constructor( ) } } + + TileListener(tiles, listening) } @Composable diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index d73dc870756b..a56fabcc7dc3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -84,7 +84,9 @@ import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.panels.ui.compose.BounceableInfo import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileEndPadding import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileStartPadding import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileUiState @@ -270,7 +272,7 @@ fun TileContainer( iconOnly = iconOnly, ) .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE) - .tilePadding(), + .thenIf(!iconOnly) { Modifier.largeTilePadding() }, // Icon tiles are center aligned content = content, ) } @@ -284,7 +286,7 @@ fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) { .clip(TileDefaults.animateTileShapeAsState(state = uiState.state).value) .background(colors.background) .height(TileHeight) - .tilePadding() + .largeTilePadding() ) { LargeTileContent( label = uiState.label, @@ -311,8 +313,8 @@ fun tileHorizontalArrangement(): Arrangement.Horizontal { return spacedBy(space = CommonTileDefaults.TileArrangementPadding, alignment = Alignment.Start) } -fun Modifier.tilePadding(): Modifier { - return padding(CommonTileDefaults.TilePadding) +fun Modifier.largeTilePadding(): Modifier { + return padding(start = TileStartPadding, end = TileEndPadding) } @Composable @@ -356,10 +358,10 @@ private object TileDefaults { val ActiveIconCornerRadius = 16.dp val ActiveTileCornerRadius = 24.dp - /** An active tile without dual target uses the active color as background */ + /** An active icon tile uses the active color as background */ @Composable @ReadOnlyComposable - fun activeTileColors(): TileColors = + fun activeIconTileColors(): TileColors = TileColors( background = MaterialTheme.colorScheme.primary, iconBackground = MaterialTheme.colorScheme.primary, @@ -418,10 +420,10 @@ private object TileDefaults { fun getColorForState(uiState: TileUiState, iconOnly: Boolean): TileColors { return when (uiState.state) { STATE_ACTIVE -> { - if (uiState.handlesSecondaryClick && !iconOnly) { + if (!iconOnly) { activeDualTargetTileColors() } else { - activeTileColors() + activeIconTileColors() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index 699e5f6b77e9..153238fc91c9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.ui.compose.selection import androidx.compose.animation.animateColor import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateOffset import androidx.compose.animation.core.animateSize import androidx.compose.animation.core.updateTransition @@ -61,6 +62,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex import com.android.compose.modifiers.size +import com.android.compose.modifiers.thenIf import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeSize @@ -184,18 +186,37 @@ private fun Modifier.selectionBorder( } } +/** + * Draws a clickable badge in the top end corner of the parent composable. + * + * The badge will fade in and fade out based on whether or not it's enabled. + * + * @param icon the [ImageVector] to display in the badge + * @param contentDescription the content description for the icon + * @param enabled Whether the badge should be visible and clickable + * @param onClick the callback when the badge is clicked + */ @Composable -fun StaticTileBadge(icon: ImageVector, contentDescription: String?, onClick: () -> Unit) { +fun StaticTileBadge( + icon: ImageVector, + contentDescription: String?, + enabled: Boolean, + onClick: () -> Unit, +) { val offset = with(LocalDensity.current) { Offset(BadgeXOffset.toPx(), BadgeYOffset.toPx()) } + val alpha by animateFloatAsState(if (enabled) 1f else 0f) MinimumInteractiveSizeComponent(angle = { BADGE_ANGLE_RAD }, offset = { offset }) { Box( Modifier.fillMaxSize() - .clickable( - interactionSource = null, - indication = null, - onClickLabel = contentDescription, - onClick = onClick, - ) + .graphicsLayer { this.alpha = alpha } + .thenIf(enabled) { + Modifier.clickable( + interactionSource = null, + indication = null, + onClickLabel = contentDescription, + onClick = onClick, + ) + } ) { val secondaryColor = MaterialTheme.colorScheme.secondary Icon( @@ -214,7 +235,8 @@ fun StaticTileBadge(icon: ImageVector, contentDescription: String?, onClick: () private fun MinimumInteractiveSizeComponent( angle: () -> Float, offset: () -> Offset, - content: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit = {}, ) { // Use a higher zIndex than the tile to draw over it, and manually create the touch target // as we're drawing over neighbor tiles as well. @@ -222,7 +244,8 @@ private fun MinimumInteractiveSizeComponent( Box( contentAlignment = Alignment.Center, modifier = - Modifier.zIndex(2f) + modifier + .zIndex(2f) .systemGestureExclusion { Rect(Offset.Zero, it.size.toSize()) } .layout { measurable, constraints -> val size = minTouchTargetSize.roundToPx() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt index 360266a31be3..99f52c28a137 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt @@ -46,8 +46,10 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) { Spacer(modifier = Modifier.weight(1f)) - viewModel.powerButtonViewModel?.let { - IconButton(it, useModifierBasedExpandable = true, Modifier.sysuiResTag("pm_lite")) - } + IconButton( + { viewModel.powerButtonViewModel }, + useModifierBasedExpandable = true, + Modifier.sysuiResTag("pm_lite"), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt index c0441f8a38a1..78fd8c0168dd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt @@ -21,13 +21,13 @@ import androidx.compose.runtime.Immutable import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.splitInRowsSequence import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel +import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.CategoryAndName /** Represents an item from a grid associated with a row and a span */ sealed interface GridCell { val row: Int val span: GridItemSpan - val s: String } /** @@ -40,7 +40,6 @@ data class TileGridCell( override val row: Int, override val width: Int, override val span: GridItemSpan = GridItemSpan(width), - override val s: String = "${tile.tileSpec.spec}-$row-$width", val column: Int, ) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile { val key: String = "${tile.tileSpec.spec}-$row" @@ -52,12 +51,23 @@ data class TileGridCell( ) : this(tile = sizedTile.tile, row = row, column = column, width = sizedTile.width) } +/** + * Represents a [EditTileViewModel] from the edit mode available tiles grid and whether it is + * available to add or not. + */ +@Immutable +data class AvailableTileGridCell( + override val tile: EditTileViewModel, + override val width: Int = 1, + val isAvailable: Boolean = true, + val key: TileSpec = tile.tileSpec, +) : SizedTile<EditTileViewModel>, CategoryAndName by tile + /** Represents an empty space used to fill incomplete rows. Will always display as a 1x1 tile */ @Immutable data class SpacerGridCell( override val row: Int, override val span: GridItemSpan = GridItemSpan(1), - override val s: String = "spacer", ) : GridCell /** diff --git a/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java index 7a6426c741a6..4be35f147c2f 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/LauncherProxyService.java @@ -830,7 +830,8 @@ public class LauncherProxyService implements CallbackController<LauncherProxyLis private void notifySystemUiStateFlags(@SystemUiStateFlags long flags, int displayId) { if (SysUiState.DEBUG) { Log.d(TAG_OPS, "Notifying sysui state change to launcher service: proxy=" - + mLauncherProxy + " flags=" + flags + " displayId=" + displayId); + + mLauncherProxy + " display=" + displayId + " flags=" + + QuickStepContract.getSystemUiStateString(flags) + " displayId=" + displayId); } try { if (mLauncherProxy != null) { diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt index b0fb6193ee45..634323c1fe2e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt @@ -83,7 +83,9 @@ object SceneContainerFlag { * testing. */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, DESCRIPTION) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, DESCRIPTION) /** Returns a developer-readable string that describes the current requirement list. */ @JvmStatic diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt index 9208fc3dd016..271f4dce0aab 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentCreator.kt @@ -23,18 +23,31 @@ import android.content.ContentProvider import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException import android.net.Uri import android.os.UserHandle import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.res.R import com.android.systemui.screenshot.scroll.LongScreenshotActivity import com.android.systemui.shared.Flags.usePreferredImageEditor +import java.util.function.Consumer import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @SysUISingleton class ActionIntentCreator @Inject -constructor(private val context: Context, private val packageManager: PackageManager) { +constructor( + private val context: Context, + private val packageManager: PackageManager, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { /** @return a chooser intent to share the given URI. */ fun createShare(uri: Uri): Intent = createShare(uri, subject = null, text = null) @@ -76,11 +89,16 @@ constructor(private val context: Context, private val packageManager: PackageMan .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } + // Non-suspend version for java compat + fun createEdit(rawUri: Uri, consumer: Consumer<Intent>) { + applicationScope.launch { consumer.accept(createEdit(rawUri)) } + } + /** * @return an ACTION_EDIT intent for the given URI, directed to config_preferredScreenshotEditor * if enabled, falling back to config_screenshotEditor if that's non-empty. */ - fun createEdit(rawUri: Uri): Intent { + suspend fun createEdit(rawUri: Uri): Intent { val uri = uriWithoutUserId(rawUri) val editIntent = Intent(Intent.ACTION_EDIT) @@ -112,22 +130,30 @@ constructor(private val context: Context, private val packageManager: PackageMan .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) } - private fun preferredEditor(): ComponentName? = + private suspend fun preferredEditor(): ComponentName? = runCatching { val preferredEditor = context.getString(R.string.config_preferredScreenshotEditor) val component = ComponentName.unflattenFromString(preferredEditor) ?: return null + return if (isComponentAvailable(component)) component else null + } + .getOrNull() + + private suspend fun isComponentAvailable(component: ComponentName): Boolean = + withContext(backgroundDispatcher) { + try { val info = packageManager.getPackageInfo( component.packageName, PackageManager.GET_ACTIVITIES, ) - - return info.activities - ?.firstOrNull { it.componentName.className.equals(component.className) } - ?.componentName + info.activities?.firstOrNull { + it.componentName.className == component.className + } != null + } catch (e: NameNotFoundException) { + false } - .getOrNull() + } private fun defaultEditor(): ComponentName? = runCatching { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt index 4373389f4a51..d91f267ff3ca 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -22,6 +22,7 @@ import android.net.Uri import android.util.Log import androidx.appcompat.content.res.AppCompatResources import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.DebugLogger.debugLog import com.android.systemui.res.R import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_EDIT_TAPPED @@ -34,6 +35,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Provides actions for screenshots. This class can be overridden by a vendor-specific SysUI @@ -68,6 +71,7 @@ constructor( private val context: Context, private val uiEventLogger: UiEventLogger, private val actionIntentCreator: ActionIntentCreator, + @Application private val applicationScope: CoroutineScope, @Assisted val requestId: UUID, @Assisted val request: ScreenshotData, @Assisted val actionExecutor: ActionExecutor, @@ -75,7 +79,7 @@ constructor( ) : ScreenshotActionsProvider { private var addedScrollChip = false private var onScrollClick: Runnable? = null - private var pendingAction: ((ScreenshotSavedResult) -> Unit)? = null + private var pendingAction: (suspend (ScreenshotSavedResult) -> Unit)? = null private var result: ScreenshotSavedResult? = null private var webUri: Uri? = null @@ -166,15 +170,16 @@ constructor( return } this.result = result - pendingAction?.invoke(result) + pendingAction?.also { applicationScope.launch { it.invoke(result) } } } override fun onAssistContent(assistContent: AssistContent?) { webUri = assistContent?.webUri } - private fun onDeferrableActionTapped(onResult: (ScreenshotSavedResult) -> Unit) { - result?.let { onResult.invoke(it) } ?: run { pendingAction = onResult } + private fun onDeferrableActionTapped(onResult: suspend (ScreenshotSavedResult) -> Unit) { + result?.let { applicationScope.launch { onResult.invoke(it) } } + ?: run { pendingAction = onResult } } @AssistedFactory @@ -188,6 +193,6 @@ constructor( } companion object { - private const val TAG = "ScreenshotActionsProvider" + private const val TAG = "ScreenshotActionsPrvdr" } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java index ecea30f1b1c3..88ffd4fa2750 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java @@ -352,30 +352,35 @@ public class LongScreenshotActivity extends Activity { private void doEdit(Uri uri) { if (mScreenshotUserHandle != Process.myUserHandle()) { // TODO: Fix transition for work profile. Omitting it in the meantime. - mActionExecutor.launchIntentAsync( - mActionIntentCreator.createEdit(uri), - mScreenshotUserHandle, false, - /* activityOptions */ null, /* transitionCoordinator */ null); + mActionIntentCreator.createEdit(uri, intent -> { + mActionExecutor.launchIntentAsync( + intent, + mScreenshotUserHandle, false, + /* activityOptions */ null, /* transitionCoordinator */ null); + }); + } else { if (usePreferredImageEditor()) { - Intent intent = mActionIntentCreator.createEdit(uri); - Bundle options = null; - - if (intent.getComponent() != null) { - // Modify intent for shared transition if we're opening a specific editor. - intent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.removeFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - mTransitionView.setImageBitmap(mOutputBitmap); - mTransitionView.setVisibility(View.VISIBLE); - mTransitionView.setTransitionName( - ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); - options = ActivityOptions.makeSceneTransitionAnimation(this, mTransitionView, - ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle(); - // TODO: listen for transition completing instead of finishing onStop - mTransitionStarted = true; - } + mActionIntentCreator.createEdit(uri, intent -> { + Bundle options = null; + + if (intent.getComponent() != null) { + // Modify intent for shared transition if we're opening a specific editor. + intent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.removeFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + mTransitionView.setImageBitmap(mOutputBitmap); + mTransitionView.setVisibility(View.VISIBLE); + mTransitionView.setTransitionName( + ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME); + options = ActivityOptions.makeSceneTransitionAnimation(this, + mTransitionView, + ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME).toBundle(); + // TODO: listen for transition completing instead of finishing onStop + mTransitionStarted = true; + } - startActivity(intent, options); + startActivity(intent, options); + }); } else { String editorPackage = getString(R.string.config_screenshotEditor); Intent intent = new Intent(Intent.ACTION_EDIT); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 17fb50aa6890..24e7976011f4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -140,6 +140,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.settings.brightness.data.repository.BrightnessMirrorShowingRepository; import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor; import com.android.systemui.shade.data.repository.FlingInfo; +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository; import com.android.systemui.shade.data.repository.ShadeRepository; import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor; import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround; @@ -204,6 +205,7 @@ import dalvik.annotation.optimization.NeverCompile; import com.google.android.msdl.data.model.MSDLToken; import com.google.android.msdl.domain.MSDLPlayer; +import dagger.Lazy; import kotlin.Unit; import kotlinx.coroutines.CoroutineDispatcher; @@ -394,7 +396,7 @@ public final class NotificationPanelViewController implements /** Whether the notifications are displayed full width (no margins on the side). */ private boolean mIsFullWidth; private boolean mBlockingExpansionForCurrentTouch; - // Following variables maintain state of events when input focus transfer may occur. + // Following variables maintain state of events when input focus transfer may occur. private boolean mExpectingSynthesizedDown; private boolean mLastEventSynthesizedDown; @@ -456,6 +458,7 @@ public final class NotificationPanelViewController implements @Deprecated // Use SysUIStateInteractor instead private final SysUiState mSysUiState; private final SysUIStateDisplaysInteractor mSysUIStateDisplaysInteractor; + private final Lazy<ShadeDisplaysRepository> mShadeDisplaysRepository; private final NotificationShadeDepthController mDepthController; private final NavigationBarController mNavigationBarController; private final int mDisplayId; @@ -637,7 +640,8 @@ public final class NotificationPanelViewController implements KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm, MSDLPlayer msdlPlayer, BrightnessMirrorShowingRepository brightnessMirrorShowingRepository, - BlurConfig blurConfig) { + BlurConfig blurConfig, + Lazy<ShadeDisplaysRepository> shadeDisplaysRepository) { mBlurConfig = blurConfig; SceneContainerFlag.assertInLegacyMode(); keyguardStateController.addCallback(new KeyguardStateController.Callback() { @@ -745,6 +749,7 @@ public final class NotificationPanelViewController implements mTapAgainViewController = tapAgainViewController; mSysUiState = sysUiState; mSysUIStateDisplaysInteractor = sysUIStateDisplaysInteractor; + mShadeDisplaysRepository = shadeDisplaysRepository; mKeyguardBypassController = bypassController; mUpdateMonitor = keyguardUpdateMonitor; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; @@ -2716,8 +2721,17 @@ public final class NotificationPanelViewController implements } private int getShadeDisplayId() { - if (mView != null && mView.getDisplay() != null) return mView.getDisplay().getDisplayId(); - return Display.DEFAULT_DISPLAY; + if (ShadeWindowGoesAround.isEnabled()) { + var pendingDisplayId = + mShadeDisplaysRepository.get().getPendingDisplayId().getValue(); + // Use the pendingDisplayId from the repository, *not* the Shade's context. + // This ensures correct UI state updates also if this method is called just *before* + // the Shade window moves to another display. + // The pendingDisplayId is guaranteed to be updated before this method is called. + return pendingDisplayId; + } else { + return Display.DEFAULT_DISPLAY; + } } private void setPerDisplaySysUIStateFlags() { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index 96b224fbd4f3..cd224735cc62 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -36,6 +36,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView +import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl import com.android.systemui.shade.display.ShadeDisplayPolicyModule @@ -205,7 +206,18 @@ object ShadeDisplayAwareModule { @SysUISingleton @Provides - fun provideShadePositionRepository(impl: ShadeDisplaysRepositoryImpl): ShadeDisplaysRepository { + fun provideShadePositionRepository( + impl: MutableShadeDisplaysRepository + ): ShadeDisplaysRepository { + ShadeWindowGoesAround.isUnexpectedlyInLegacyMode() + return impl + } + + @SysUISingleton + @Provides + fun provideMutableShadePositionRepository( + impl: ShadeDisplaysRepositoryImpl + ): MutableShadeDisplaysRepository { ShadeWindowGoesAround.isUnexpectedlyInLegacyMode() return impl } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt index 79e63330e3a5..63b618f72921 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt @@ -50,7 +50,9 @@ object ShadeExpandsOnStatusBarLongPress { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt index 3513334f2a5c..e18ed83a1e8e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FakeShadeDisplayRepository.kt @@ -22,16 +22,28 @@ import com.android.systemui.shade.display.ShadeDisplayPolicy import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class FakeShadeDisplayRepository : ShadeDisplaysRepository { +class FakeShadeDisplayRepository : MutableShadeDisplaysRepository { private val _displayId = MutableStateFlow(Display.DEFAULT_DISPLAY) + private val _pendingDisplayId = MutableStateFlow(Display.DEFAULT_DISPLAY) fun setDisplayId(displayId: Int) { _displayId.value = displayId } + fun setPendingDisplayId(displayId: Int) { + _pendingDisplayId.value = displayId + } + + override fun onDisplayChangedSucceeded(displayId: Int) { + setDisplayId(displayId) + } + override val displayId: StateFlow<Int> get() = _displayId + override val pendingDisplayId: StateFlow<Int> + get() = _pendingDisplayId + override val currentPolicy: ShadeDisplayPolicy get() = FakeShadeDisplayPolicy } diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt index 2a14ca44386d..7117a23bfb77 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt @@ -29,6 +29,7 @@ import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -40,10 +41,28 @@ import kotlinx.coroutines.flow.stateIn /** Source of truth for the display currently holding the shade. */ interface ShadeDisplaysRepository { - /** ID of the display which currently hosts the shade */ + /** ID of the display which currently hosts the shade. */ val displayId: StateFlow<Int> /** The current policy set. */ val currentPolicy: ShadeDisplayPolicy + + /** + * Id of the display that should host the shade. + * + * If this differs from [displayId], it means there is a shade movement in progress. Classes + * that rely on the shade being already moved (and its context/resources updated) should rely on + * [displayId]. Classes that need to do work associated with the shade move, should listen at + * this. + */ + val pendingDisplayId: StateFlow<Int> +} + +/** Provides a way to set whether the display changed succeeded. */ +interface MutableShadeDisplaysRepository : ShadeDisplaysRepository { + /** + * To be called when the shade changed window, and its resources have been completely updated. + */ + fun onDisplayChangedSucceeded(displayId: Int) } /** @@ -63,7 +82,7 @@ constructor( @ShadeOnDefaultDisplayWhenLocked private val shadeOnDefaultDisplayWhenLocked: Boolean, keyguardRepository: KeyguardRepository, displayRepository: DisplayRepository, -) : ShadeDisplaysRepository { +) : MutableShadeDisplaysRepository { private val policy: StateFlow<ShadeDisplayPolicy> = globalSettings @@ -105,10 +124,16 @@ constructor( override val currentPolicy: ShadeDisplayPolicy get() = policy.value - override val displayId: StateFlow<Int> = + override val pendingDisplayId: StateFlow<Int> = keyguardAwareDisplayPolicy.stateIn( bgScope, SharingStarted.WhileSubscribed(), Display.DEFAULT_DISPLAY, ) + private val _committedDisplayId = MutableStateFlow(Display.DEFAULT_DISPLAY) + override val displayId: StateFlow<Int> = _committedDisplayId + + override fun onDisplayChangedSucceeded(displayId: Int) { + _committedDisplayId.value = displayId + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt index 9a5c96824e77..0e0f58dc8d0e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker import com.android.systemui.shade.ShadeTraceLogger.logMoveShadeWindowTo import com.android.systemui.shade.ShadeTraceLogger.t import com.android.systemui.shade.ShadeTraceLogger.traceReparenting +import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.display.ShadeExpansionIntent import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround @@ -44,6 +45,7 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -55,7 +57,7 @@ import kotlinx.coroutines.withTimeoutOrNull class ShadeDisplaysInteractor @Inject constructor( - private val shadePositionRepository: ShadeDisplaysRepository, + private val shadePositionRepository: MutableShadeDisplaysRepository, @ShadeDisplayAware private val shadeContext: WindowContext, @ShadeDisplayAware private val configurationRepository: ConfigurationRepository, @Background private val bgScope: CoroutineScope, @@ -72,11 +74,14 @@ constructor( private val hasActiveNotifications: Boolean get() = activeNotificationsInteractor.areAnyNotificationsPresentValue + /** Current display id of the shade window. */ + val displayId: StateFlow<Int> = shadePositionRepository.displayId + override fun start() { ShadeWindowGoesAround.isUnexpectedlyInLegacyMode() listenForWindowContextConfigChanges() bgScope.launchTraced(TAG) { - shadePositionRepository.displayId.collectLatest { displayId -> + shadePositionRepository.pendingDisplayId.collectLatest { displayId -> moveShadeWindowTo(displayId) } } @@ -108,7 +113,8 @@ constructor( val currentDisplay = shadeContext.display ?: error("Current shade display is null") currentId = currentDisplay.displayId if (currentId == destinationId) { - error("Trying to move the shade to a display it was already in") + Log.w(TAG, "Trying to move the shade to a display ($currentId) it was already in ") + return } withContext(mainThreadContext) { @@ -118,6 +124,7 @@ constructor( reparentToDisplayId(id = destinationId) } checkContextDisplayMatchesExpected(destinationId) + shadePositionRepository.onDisplayChangedSucceeded(destinationId) } } } catch (e: IllegalStateException) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt index 016f50f471de..81824f69eddd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt @@ -67,7 +67,9 @@ object ShadeWindowGoesAround { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index bfd512fa6a2d..ef0660fbcd1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -566,12 +566,11 @@ public final class KeyboardShortcutListSearch { Arrays.asList( Pair.create(KeyEvent.KEYCODE_TAB, KeyEvent.META_META_ON))), /* Back: go back to previous state (back button) */ - /* Meta + Escape, Meta + backspace, Meta + left arrow */ + /* Meta + Escape, Meta + left arrow */ new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_go_back), Arrays.asList( 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 + S */ new ShortcutKeyGroupMultiMappingInfo( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index c9b96e9871ed..ec04edb0f810 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -76,6 +76,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController.StateList import com.android.systemui.recents.LauncherProxyService; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; @@ -810,6 +811,10 @@ public class NotificationLockscreenUserManagerImpl implements * lock time. */ private boolean shouldShowSensitiveContentRedactedView(NotificationEntry ent) { + if (android.app.Flags.redactionOnLockscreenMetrics()) { + return shouldShowSensitiveContentRedactedViewWithLog(ent); + } + if (!LockscreenOtpRedaction.isEnabled()) { return false; } @@ -846,6 +851,71 @@ public class NotificationLockscreenUserManagerImpl implements return true; } + /* + * We show the sensitive content redaction view if + * 1. The feature is enabled + * 2. The notification has the `hasSensitiveContent` ranking variable set to true + * 3. The device is locked + * 4. The device is NOT connected to Wifi + * 5. The device has not connected to Wifi since receiving the notification + * 6. The notification arrived at least LOCK_TIME_FOR_SENSITIVE_REDACTION_MS after the last + * lock time. + * + * This version of the method logs a metric about the request. + */ + private boolean shouldShowSensitiveContentRedactedViewWithLog(NotificationEntry ent) { + if (!LockscreenOtpRedaction.isEnabled()) { + return false; + } + + if (ent.getRanking() == null || !ent.getRanking().hasSensitiveContent()) { + return false; + } + + long notificationWhen = ent.getSbn().getNotification().getWhen(); + long notificationTime = getEarliestNotificationTime(ent); + boolean locked = mLocked.get(); + long lockTime = mLastLockTime.get(); + boolean wifiConnected = mConnectedToWifi.get(); + long wifiConnectionTime = mLastWifiConnectionTime.get(); + + boolean shouldRedact = true; + if (!locked) { + shouldRedact = false; + } + + if (!mRedactOtpOnWifi.get()) { + if (wifiConnected) { + shouldRedact = false; + } + + // If the device has connected to wifi since receiving the notification, do not redact + if (notificationTime < wifiConnectionTime) { + shouldRedact = false; + } + } + + // If the lock screen was not already locked for at least mOtpRedactionRequiredLockTimeMs + // when this notification arrived, do not redact + long latestTimeForRedaction = lockTime + mOtpRedactionRequiredLockTimeMs.get(); + + if (notificationTime < latestTimeForRedaction) { + shouldRedact = false; + } + + int whenAndEarliestDiff = clampLongToIntRange(notificationWhen - notificationTime); + int earliestAndLockDiff = clampLongToIntRange(lockTime - notificationTime); + int earliestAndWifiDiff = clampLongToIntRange(wifiConnectionTime - notificationTime); + SysUiStatsLog.write(SysUiStatsLog.OTP_NOTIFICATION_DISPLAYED, shouldRedact, + whenAndEarliestDiff, locked, earliestAndLockDiff, wifiConnected, + earliestAndWifiDiff); + return shouldRedact; + } + + private int clampLongToIntRange(long toConvert) { + return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, toConvert)); + } + // Get the earliest time the user might have seen this notification. This is either the // notification's "when" time, or the notification entry creation time private long getEarliestNotificationTime(NotificationEntry notif) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index bbd8ad4a61fc..bdfdb8191356 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -38,13 +38,13 @@ import com.android.systemui.Flags import com.android.systemui.Flags.spatialModelAppPushback import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shared.Flags.ambientAod import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK import com.android.systemui.statusbar.phone.DozeParameters @@ -53,7 +53,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.WallpaperController -import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.wm.shell.appzoomout.AppZoomOut import java.io.PrintWriter @@ -79,14 +78,12 @@ constructor( private val keyguardInteractor: KeyguardInteractor, private val choreographer: Choreographer, private val wallpaperController: WallpaperController, - private val wallpaperInteractor: WallpaperInteractor, private val notificationShadeWindowController: NotificationShadeWindowController, private val dozeParameters: DozeParameters, @ShadeDisplayAware private val context: Context, private val splitShadeStateController: SplitShadeStateController, private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor, private val appZoomOutOptional: Optional<AppZoomOut>, - @Application private val applicationScope: CoroutineScope, dumpManager: DumpManager, configurationController: ConfigurationController, ) : ShadeExpansionListener, Dumpable { @@ -116,8 +113,6 @@ constructor( private var prevTimestamp: Long = -1 private var prevShadeDirection = 0 private var prevShadeVelocity = 0f - private var prevDozeAmount: Float = 0f - @VisibleForTesting var wallpaperSupportsAmbientMode: Boolean = false // tracks whether app launch transition is in progress. This involves two independent factors // that control blur, shade expansion and app launch animation from outside sysui. // They can complete out of order, this flag will be reset by the animation that finishes later. @@ -234,15 +229,7 @@ constructor( } /** Blur radius of the wake-up animation on this frame. */ - private var wakeBlurRadius = 0f - set(value) { - if (field == value) return - field = value - scheduleUpdate() - } - - /** Blur radius of the unlock animation on this frame. */ - private var unlockBlurRadius = 0f + private var wakeAndUnlockBlurRadius = 0f set(value) { if (field == value) return field = value @@ -269,16 +256,14 @@ constructor( ShadeInterpolation.getNotificationScrimAlpha(qsPanelExpansion) * shadeExpansion combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(qsExpandedRatio)) combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(transitionToFullShadeProgress)) - var shadeRadius = max(combinedBlur, max(wakeBlurRadius, unlockBlurRadius)) + var shadeRadius = max(combinedBlur, wakeAndUnlockBlurRadius) if (areBlursDisabledForAppLaunch || blursDisabledForUnlock) { shadeRadius = 0f } var blur = shadeRadius.toInt() - // If the blur comes from waking up, we don't want to zoom out the background - val zoomOut = - if (shadeRadius != wakeBlurRadius) blurRadiusToZoomOut(blurRadius = shadeRadius) else 0f + val zoomOut = blurRadiusToZoomOut(blurRadius = shadeRadius) // Make blur be 0 if it is necessary to stop blur effect. if (scrimsVisible) { if (!Flags.notificationShadeBlur()) { @@ -363,14 +348,14 @@ constructor( startDelay = keyguardStateController.keyguardFadingAwayDelay interpolator = Interpolators.FAST_OUT_SLOW_IN addUpdateListener { animation: ValueAnimator -> - unlockBlurRadius = + wakeAndUnlockBlurRadius = blurUtils.blurRadiusOfRatio(animation.animatedValue as Float) } addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { keyguardAnimator = null - unlockBlurRadius = 0f + wakeAndUnlockBlurRadius = 0f } } ) @@ -406,20 +391,15 @@ constructor( } override fun onDozeAmountChanged(linear: Float, eased: Float) { - prevDozeAmount = eased - updateWakeBlurRadius(prevDozeAmount) + wakeAndUnlockBlurRadius = + if (ambientAod()) { + 0f + } else { + blurUtils.blurRadiusOfRatio(eased) + } } } - private fun updateWakeBlurRadius(ratio: Float) { - wakeBlurRadius = - if (!wallpaperSupportsAmbientMode) { - 0f - } else { - blurUtils.blurRadiusOfRatio(ratio) - } - } - init { dumpManager.registerCriticalDumpable(javaClass.name, this) if (WAKE_UP_ANIMATION_ENABLED) { @@ -441,12 +421,6 @@ constructor( } } ) - applicationScope.launch { - wallpaperInteractor.wallpaperSupportsAmbientMode.collect { supported -> - wallpaperSupportsAmbientMode = supported - updateWakeBlurRadius(prevDozeAmount) - } - } initBlurListeners() } @@ -637,8 +611,7 @@ constructor( it.println("shouldApplyShadeBlur: ${shouldApplyShadeBlur()}") it.println("shadeAnimation: ${shadeAnimation.radius}") it.println("brightnessMirrorRadius: ${brightnessMirrorSpring.radius}") - it.println("wakeBlur: $wakeBlurRadius") - it.println("unlockBlur: $wakeBlurRadius") + it.println("wakeAndUnlockBlur: $wakeAndUnlockBlurRadius") it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch") it.println("appLaunchTransitionIsInProgress: $appLaunchTransitionIsInProgress") it.println("qsPanelExpansion: $qsPanelExpansion") 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 d8c3e2546a8f..a0de879845d3 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 @@ -56,7 +56,7 @@ constructor( private val logger = Logger(logBuffer, "Notif".pad()) // [StatusBarChipLogTag] recommends a max tag length of 20, so [extraLogTag] should NOT be the // top-level tag. It should instead be provided as the first string in each log message. - private val extraLogTag = "SingleChipInteractor[key=$key]" + private val extraLogTag = "SingleNotifChipInteractor[key=$key][id=${hashCode()}]" init { if (startingModel.promotedContent == null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt index 35e622ba8e44..9380dfe32bf5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn /** An interactor for the notification chips shown in the status bar. */ @SysUISingleton @@ -147,28 +149,42 @@ constructor( */ val allNotificationChips: Flow<List<NotificationChipModel>> = if (StatusBarNotifChips.isEnabled) { - // For all our current interactors... - // TODO(b/364653005): When a promoted notification is added or removed, each individual - // interactor's [notificationChip] flow becomes un-collected then re-collected, which - // can cause some flows to remove then add callbacks when they don't need to. Is there a - // better structure for this? Maybe Channels or a StateFlow with a short timeout? - promotedNotificationInteractors.flatMapLatest { interactors -> - if (interactors.isNotEmpty()) { - // Combine each interactor's [notificationChip] flow... - val allNotificationChips: List<Flow<NotificationChipModel?>> = - interactors.map { interactor -> interactor.notificationChip } - combine(allNotificationChips) { + // For all our current interactors... + promotedNotificationInteractors.flatMapLatest { interactors -> + if (interactors.isNotEmpty()) { + // Combine each interactor's [notificationChip] flow... + val allNotificationChips: List<Flow<NotificationChipModel?>> = + interactors.map { interactor -> interactor.notificationChip } + combine(allNotificationChips) { // ... and emit just the non-null & sorted chips it.filterNotNull().sortedWith(chipComparator) } - .logSort() - } else { - flowOf(emptyList()) + } else { + flowOf(emptyList()) + } } + } else { + flowOf(emptyList()) } - } else { - flowOf(emptyList()) - } + .distinctUntilChanged() + .logSort() + .stateIn( + backgroundScope, + SharingStarted.WhileSubscribed( + // When a promoted notification is added or removed, the `.flatMapLatest` above + // will stop collection and then re-start collection on each individual + // interactor's flow. (It will happen even for a chip that didn't change.) We + // don't want the individual interactor flows to stop then re-start because they + // may be maintaining values that would get thrown away when collection stops + // (like an app's last visible time). + // stopTimeoutMillis ensures we maintain those values even during the brief + // moment (1-2ms) when `.flatMapLatest` causes collect to stop then immediately + // restart. + // Note: Channels could also work to solve this. + stopTimeoutMillis = 1000 + ), + initialValue = emptyList(), + ) /** Emits the notifications that should actually be *shown* as chips in the status bar. */ val shownNotificationChips: Flow<List<NotificationChipModel>> = @@ -199,14 +215,14 @@ constructor( } private fun Flow<List<NotificationChipModel>>.logSort(): Flow<List<NotificationChipModel>> { - return this.distinctUntilChanged().onEach { chips -> + return this.onEach { chips -> val logString = chips.joinToString { "{key=${it.key}. " + "lastVisibleAppTime=${it.lastAppVisibleTime}. " + "creationTime=${it.creationTime}}" } - logger.d({ "Sorted chips: $str1" }) { str1 = logString } + logger.d({ "Sorted notif chips: $str1" }) { str1 = logString } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt index 947e93c8a432..6431f303089f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt @@ -50,7 +50,9 @@ object StatusBarNotifChips { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if 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 e8ab39692558..d81ea07cae2d 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 @@ -75,7 +75,12 @@ fun OngoingActivityChip( } } is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { - ChipBody(model, iconViewStore, onClick = { clickBehavior.onClick() }) + ChipBody( + model, + iconViewStore, + onClick = { clickBehavior.onClick() }, + modifier = modifier, + ) } is OngoingActivityChipModel.ClickBehavior.None -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt index 3b8c0f48e40e..7080c3402b08 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource +import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder @@ -47,7 +48,13 @@ fun OngoingActivityChips( chips.active .filter { !it.isHidden } .forEach { - key(it.key) { OngoingActivityChip(model = it, iconViewStore = iconViewStore) } + key(it.key) { + OngoingActivityChip( + model = it, + iconViewStore = iconViewStore, + modifier = Modifier.sysuiResTag(it.key), + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index 1a30caf0150b..eae2c25d77d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -350,6 +350,26 @@ constructor( .stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModelLegacy()) } + private val activeChips = + if (StatusBarChipsModernization.isEnabled) { + chips.map { it.active } + } else { + chipsLegacy.map { + val list = mutableListOf<OngoingActivityChipModel.Active>() + if (it.primary is OngoingActivityChipModel.Active) { + list.add(it.primary) + } + if (it.secondary is OngoingActivityChipModel.Active) { + list.add(it.secondary) + } + list + } + } + + /** A flow modeling just the keys for the currently visible chips. */ + val visibleChipKeys: Flow<List<String>> = + activeChips.map { chips -> chips.filter { !it.isHidden }.map { it.key } } + /** * Sort the given chip [bundle] in order of priority, and divide the chips between active, * overflow, and inactive (see [MultipleOngoingActivityChipsModel] for a description of each). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt index 9e1686ae135f..03316d3e326c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt @@ -49,7 +49,9 @@ object NewStatusBarIcons { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConnectedDisplays.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConnectedDisplays.kt index cb1827d57c2a..a58e00c5ee0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConnectedDisplays.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarConnectedDisplays.kt @@ -50,7 +50,9 @@ object StatusBarConnectedDisplays { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt index 4ee49d82b2fd..25dace32261e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt @@ -52,7 +52,9 @@ object StatusBarRootModernization { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt index 27d815190d85..9db2f4b46dae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt @@ -22,6 +22,7 @@ import com.android.systemui.statusbar.data.repository.RemoteInputRepositoryModul import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerModule import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStoreModule import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryModule +import com.android.systemui.statusbar.data.repository.StatusBarPerDisplayConfigurationStateModule import com.android.systemui.statusbar.data.repository.SystemEventChipAnimationControllerStoreModule import com.android.systemui.statusbar.phone.data.StatusBarPhoneDataLayerModule import dagger.Module @@ -34,6 +35,7 @@ import dagger.Module LightBarControllerStoreModule::class, RemoteInputRepositoryModule::class, StatusBarConfigurationControllerModule::class, + StatusBarPerDisplayConfigurationStateModule::class, StatusBarContentInsetsProviderStoreModule::class, StatusBarModeRepositoryModule::class, StatusBarPhoneDataLayerModule::class, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayConfigurationStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayConfigurationStateRepository.kt new file mode 100644 index 000000000000..3168a22c56ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarPerDisplayConfigurationStateRepository.kt @@ -0,0 +1,69 @@ +/* + * 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.data.repository + +import android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.common.ui.ConfigurationStateImpl +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository +import com.android.systemui.display.data.repository.PerDisplayInstanceProvider +import com.android.systemui.display.data.repository.PerDisplayInstanceRepositoryImpl +import com.android.systemui.display.data.repository.PerDisplayRepository +import com.android.systemui.display.data.repository.SingleInstanceRepositoryImpl +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import dagger.Lazy +import dagger.Module +import dagger.Provides +import javax.inject.Inject + +@SysUISingleton +class StatusBarPerDisplayConfigurationStateProvider +@Inject +constructor( + private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository, + private val statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore, + private val factory: ConfigurationStateImpl.Factory, +) : PerDisplayInstanceProvider<ConfigurationState> { + + override fun createInstance(displayId: Int): ConfigurationState? { + val displayWindowProperties = + displayWindowPropertiesRepository.get(displayId, TYPE_STATUS_BAR) ?: return null + val configController = + statusBarConfigurationControllerStore.forDisplay(displayId) ?: return null + return factory.create(displayWindowProperties.context, configController) + } +} + +@Module +object StatusBarPerDisplayConfigurationStateModule { + + @Provides + @SysUISingleton + fun store( + instanceProvider: Lazy<StatusBarPerDisplayConfigurationStateProvider>, + factory: PerDisplayInstanceRepositoryImpl.Factory<ConfigurationState>, + defaultInstance: ConfigurationState, + ): PerDisplayRepository<ConfigurationState> { + val name = "ConfigurationState" + return if (StatusBarConnectedDisplays.isEnabled) { + factory.create(name, instanceProvider.get()) + } else { + SingleInstanceRepositoryImpl(name, defaultInstance) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt index 90f97df295f5..ec6508717e8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt @@ -17,51 +17,51 @@ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel import android.content.Context +import androidx.compose.runtime.getValue 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.Background +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import com.android.systemui.statusbar.featurepods.popups.shared.model.HoverBehavior import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipViewModel -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch /** * [StatusBarPopupChipViewModel] for a media control chip in the status bar. This view model is * responsible for converting the [MediaControlChipModel] to a [PopupChipModel] that can be used to * display a media control chip. */ -@SysUISingleton class MediaControlChipViewModel -@Inject +@AssistedInject constructor( - @Background private val backgroundScope: CoroutineScope, @Application private val applicationContext: Context, mediaControlChipInteractor: MediaControlChipInteractor, -) : StatusBarPopupChipViewModel { - +) : StatusBarPopupChipViewModel, ExclusiveActivatable() { + private val hydrator: Hydrator = Hydrator("MediaControlChipViewModel.hydrator") /** - * A [StateFlow] of the current [PopupChipModel]. This flow emits a new [PopupChipModel] + * A snapshot [State] of the current [PopupChipModel]. This emits a new [PopupChipModel] * whenever the underlying [MediaControlChipModel] changes. */ - override val chip: StateFlow<PopupChipModel> = - mediaControlChipInteractor.mediaControlChipModel - .map { mediaControlChipModel -> toPopupChipModel(mediaControlChipModel) } - .stateIn( - backgroundScope, - SharingStarted.WhileSubscribed(), - PopupChipModel.Hidden(PopupChipId.MediaControl), - ) + override val chip: PopupChipModel by + hydrator.hydratedStateOf( + traceName = "chip", + initialValue = PopupChipModel.Hidden(PopupChipId.MediaControl), + source = + mediaControlChipInteractor.mediaControlChipModel.map { model -> + toPopupChipModel(model) + }, + ) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } private fun toPopupChipModel(model: MediaControlChipModel?): PopupChipModel { if (model == null || model.songName.isNullOrEmpty()) { @@ -96,7 +96,12 @@ constructor( return HoverBehavior.Button( icon = Icon.Loaded(drawable = icon, contentDescription = contentDescription), - onIconPressed = { backgroundScope.launch { action.run() } }, + onIconPressed = { action.run() }, ) } + + @AssistedFactory + interface Factory { + fun create(): MediaControlChipViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt index 08a8960f6c96..d1307aa0ee31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt @@ -50,7 +50,9 @@ object StatusBarPopupChips { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt index 5712be30ccd6..38f24137d355 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt @@ -16,14 +16,14 @@ package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel +import com.android.systemui.lifecycle.Activatable import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel -import kotlinx.coroutines.flow.StateFlow /** * Interface for a view model that knows the display requirements for a single type of status bar * popup chip. */ -interface StatusBarPopupChipViewModel { - /** A flow modeling the popup chip that should be shown (or not shown). */ - val chip: StateFlow<PopupChipModel> +interface StatusBarPopupChipViewModel : Activatable { + /** A snapshot [State] modeling the popup chip that should be shown (or not shown). */ + val chip: PopupChipModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt index 33bf90defb48..35f1a9981691 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.Hydrator import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.MediaControlChipViewModel import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId @@ -36,18 +35,17 @@ import kotlinx.coroutines.flow.map */ class StatusBarPopupChipsViewModel @AssistedInject -constructor(mediaControlChip: MediaControlChipViewModel) : ExclusiveActivatable() { - private val hydrator: Hydrator = Hydrator("StatusBarPopupChipsViewModel.hydrator") +constructor(mediaControlChipFactory: MediaControlChipViewModel.Factory) : ExclusiveActivatable() { + + private val mediaControlChip by lazy { mediaControlChipFactory.create() } /** The ID of the current chip that is showing its popup, or `null` if no chip is shown. */ private var currentShownPopupChipId by mutableStateOf<PopupChipId?>(null) - private val incomingPopupChipBundle: PopupChipBundle by - hydrator.hydratedStateOf( - traceName = "incomingPopupChipBundle", - initialValue = PopupChipBundle(), - source = mediaControlChip.chip.map { chip -> PopupChipBundle(media = chip) }, - ) + private val incomingPopupChipBundle: PopupChipBundle by derivedStateOf { + val mediaChip = mediaControlChip.chip + PopupChipBundle(media = mediaChip) + } val shownPopupChips: List<PopupChipModel.Shown> by derivedStateOf { if (StatusBarPopupChips.isEnabled) { @@ -66,7 +64,7 @@ constructor(mediaControlChip: MediaControlChipViewModel) : ExclusiveActivatable( } override suspend fun onActivated(): Nothing { - hydrator.activate() + mediaControlChip.activate() } private data class PopupChipBundle( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt b/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt index 7a985e7fae9a..7b1024ca65dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/headsup/shared/StatusBarNoHunBehavior.kt @@ -50,7 +50,9 @@ object StatusBarNoHunBehavior { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index 4053d065cb16..8be9e410f8f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -124,8 +124,14 @@ public final class NotificationClicker implements View.OnClickListener { Notification notification = sbn.getNotification(); if (notification.contentIntent != null || notification.fullScreenIntent != null || row.getEntry().isBubble()) { - row.setBubbleClickListener(v -> - mNotificationActivityStarter.onNotificationBubbleIconClicked(row.getEntry())); + if (NotificationBundleUi.isEnabled()) { + row.setBubbleClickListener( + v -> row.getEntryAdapter().onNotificationBubbleIconClicked()); + } else { + row.setBubbleClickListener(v -> + mNotificationActivityStarter.onNotificationBubbleIconClicked( + row.getEntry())); + } row.setOnClickListener(this); row.setOnDragSuccessListener(mOnDragSuccessListener); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt index 0a24d7a71ce9..68c13afe82dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt @@ -88,8 +88,8 @@ class PhysicsPropertyAnimator { @JvmStatic fun createDefaultSpring(): SpringForce { return SpringForce() - .setStiffness(380f) // MEDIUM LOW STIFFNESS - .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) // LOW BOUNCINESS + .setStiffness(380f) + .setDampingRatio(0.68f); } @JvmStatic diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 6dd44a123538..41353b9921bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -45,8 +45,6 @@ import kotlinx.coroutines.flow.StateFlowKt; */ public class BundleEntry extends PipelineEntry { - private final BundleEntryAdapter mEntryAdapter; - // TODO(b/394483200): move NotificationEntry's implementation to PipelineEntry? private final MutableStateFlow<Boolean> mSensitive = StateFlowKt.MutableStateFlow(false); @@ -55,7 +53,6 @@ public class BundleEntry extends PipelineEntry { public BundleEntry(String key) { super(key); - mEntryAdapter = new BundleEntryAdapter(); } @Nullable @@ -86,126 +83,9 @@ public class BundleEntry extends PipelineEntry { return false; } - @VisibleForTesting - public BundleEntryAdapter getEntryAdapter() { - return mEntryAdapter; - } - - public class BundleEntryAdapter implements EntryAdapter { - - /** - * TODO (b/394483200): convert to PipelineEntry.ROOT_ENTRY when pipeline is migrated? - */ - @Override - public GroupEntry getParent() { - return GroupEntry.ROOT_ENTRY; - } - - @Override - public boolean isTopLevelEntry() { - return true; - } - - @NonNull - @Override - public String getKey() { - return mKey; - } - - @Override - @Nullable - public ExpandableNotificationRow getRow() { - return mRow; - } - - @Override - public boolean isGroupRoot() { - return true; - } - - @Override - public StateFlow<Boolean> isSensitive() { - return BundleEntry.this.mSensitive; - } - - @Override - public boolean isClearable() { - // TODO(b/394483200): check whether all of the children are clearable, when implemented - return true; - } - - @Override - public int getTargetSdk() { - return Build.VERSION_CODES.CUR_DEVELOPMENT; - } - - @Override - public String getSummarization() { - return null; - } - - @Override - public int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor) { - return Notification.COLOR_DEFAULT; - } - - @Override - public boolean canPeek() { - return false; - } - - @Override - public long getWhen() { - return 0; - } - - @Override - public IconPack getIcons() { - // TODO(b/396446620): implement bundle icons - return null; - } - - @Override - public boolean isColorized() { - return false; - } - - @Override - @Nullable - public StatusBarNotification getSbn() { - return null; - } - - @Override - public boolean canDragAndDrop() { - return false; - } - - @Override - public boolean isBubbleCapable() { - return false; - } - - @Override - @Nullable - public String getStyle() { - return null; - } - - @Override - public int getSectionBucket() { - return mBucket; - } - - @Override - public boolean isAmbient() { - return false; - } - - @Override - public boolean isFullScreenCapable() { - return false; - } + @Nullable + public ExpandableNotificationRow getRow() { + return mRow; } public static final List<BundleEntry> ROOT_BUNDLES = List.of( @@ -213,4 +93,8 @@ public class BundleEntry extends PipelineEntry { new BundleEntry(SOCIAL_MEDIA_ID), new BundleEntry(NEWS_ID), new BundleEntry(RECS_ID)); + + public MutableStateFlow<Boolean> isSensitive() { + return mSensitive; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt new file mode 100644 index 000000000000..64db9df8270c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt @@ -0,0 +1,122 @@ +/* + * 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.notification.collection + +import android.app.Notification +import android.content.Context +import android.os.Build +import android.service.notification.StatusBarNotification +import com.android.systemui.statusbar.notification.icon.IconPack +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import kotlinx.coroutines.flow.StateFlow + +class BundleEntryAdapter(val entry: BundleEntry) : EntryAdapter { + /** TODO (b/394483200): convert to PipelineEntry.ROOT_ENTRY when pipeline is migrated? */ + override fun getParent(): GroupEntry { + return GroupEntry.ROOT_ENTRY + } + + override fun isTopLevelEntry(): Boolean { + return true + } + + override fun getKey(): String { + return entry.key + } + + override fun getRow(): ExpandableNotificationRow? { + return entry.row + } + + override fun isGroupRoot(): Boolean { + return true + } + + override fun isSensitive(): StateFlow<Boolean> { + return entry.isSensitive + } + + override fun isClearable(): Boolean { + // TODO(b/394483200): check whether all of the children are clearable, when implemented + return true + } + + override fun getTargetSdk(): Int { + return Build.VERSION_CODES.CUR_DEVELOPMENT + } + + override fun getSummarization(): String? { + return null + } + + override fun getContrastedColor( + context: Context?, + isLowPriority: Boolean, + backgroundColor: Int, + ): Int { + return Notification.COLOR_DEFAULT + } + + override fun canPeek(): Boolean { + return false + } + + override fun getWhen(): Long { + return 0 + } + + override fun getIcons(): IconPack? { + // TODO(b/396446620): implement bundle icons + return null + } + + override fun isColorized(): Boolean { + return false + } + + override fun getSbn(): StatusBarNotification? { + return null + } + + override fun canDragAndDrop(): Boolean { + return false + } + + override fun isBubbleCapable(): Boolean { + return false + } + + override fun getStyle(): String? { + return null + } + + override fun getSectionBucket(): Int { + return entry.bucket + } + + override fun isAmbient(): Boolean { + return false + } + + override fun isFullScreenCapable(): Boolean { + return false + } + + override fun onNotificationBubbleIconClicked() { + // do nothing. these cannot be a bubble + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index 307a9573d5d8..0e75b6050678 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -50,7 +50,7 @@ public interface EntryAdapter { /** * Gets the view that this entry is backing. */ - @NonNull + @Nullable ExpandableNotificationRow getRow(); /** @@ -135,5 +135,10 @@ public interface EntryAdapter { default boolean isFullScreenCapable() { return false; } + + /** + * Process a click on a notification bubble icon + */ + void onNotificationBubbleIconClicked(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactory.kt new file mode 100644 index 000000000000..b7a84ddef103 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactory.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.statusbar.notification.collection + +/** Creates an appropriate EntryAdapter for the entry type given */ +interface EntryAdapterFactory { + fun create(entry: PipelineEntry): EntryAdapter +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt new file mode 100644 index 000000000000..779c25a3b402 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.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.statusbar.notification.collection + +import androidx.annotation.VisibleForTesting +import com.android.internal.logging.MetricsLogger +import com.android.systemui.statusbar.notification.NotificationActivityStarter +import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider +import javax.inject.Inject + +/** Creates an appropriate EntryAdapter for the entry type given */ +class EntryAdapterFactoryImpl +@Inject +constructor( + private val notificationActivityStarter: NotificationActivityStarter, + private val metricsLogger: MetricsLogger, + private val peopleNotificationIdentifier: PeopleNotificationIdentifier, + private val iconStyleProvider: NotificationIconStyleProvider, + private val visualStabilityCoordinator: VisualStabilityCoordinator, +) : EntryAdapterFactory { + override fun create(entry: PipelineEntry): EntryAdapter { + return if (entry is NotificationEntry) { + NotificationEntryAdapter( + notificationActivityStarter, + metricsLogger, + peopleNotificationIdentifier, + iconStyleProvider, + visualStabilityCoordinator, + entry, + ) + } else { + BundleEntryAdapter((entry as BundleEntry)) + } + } + + @VisibleForTesting + fun getNotificationActivityStarter() : NotificationActivityStarter { + return notificationActivityStarter + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index b19ba3a18826..3d8ba7f335bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -109,7 +109,6 @@ public final class NotificationEntry extends ListEntry { private final String mKey; private StatusBarNotification mSbn; private Ranking mRanking; - private final NotifEntryAdapter mEntryAdapter; /* * Bookkeeping members @@ -270,141 +269,6 @@ public final class NotificationEntry extends ListEntry { mKey = sbn.getKey(); setSbn(sbn); setRanking(ranking); - mEntryAdapter = new NotifEntryAdapter(); - } - - public class NotifEntryAdapter implements EntryAdapter { - @Override - public PipelineEntry getParent() { - return NotificationEntry.this.getParent(); - } - - @Override - public boolean isTopLevelEntry() { - return getParent() != null - && (getParent() == ROOT_ENTRY || ROOT_BUNDLES.contains(getParent())); - } - - @Override - public String getKey() { - return NotificationEntry.this.getKey(); - } - - @Override - public ExpandableNotificationRow getRow() { - return NotificationEntry.this.getRow(); - } - - @Override - public boolean isGroupRoot() { - if (isTopLevelEntry() || getParent() == null) { - return false; - } - if (NotificationEntry.this.getParent() instanceof GroupEntry parentGroupEntry) { - return parentGroupEntry.getSummary() == NotificationEntry.this; - } - return false; - } - - @Override - public StateFlow<Boolean> isSensitive() { - return NotificationEntry.this.isSensitive(); - } - - @Override - public boolean isClearable() { - return NotificationEntry.this.isClearable(); - } - - @Override - public int getTargetSdk() { - return NotificationEntry.this.targetSdk; - } - - @Override - public String getSummarization() { - return getRanking().getSummarization(); - } - - @Override - public void prepareForInflation() { - getSbn().clearPackageContext(); - } - - @Override - public int getContrastedColor(Context context, boolean isLowPriority, int backgroundColor) { - return NotificationEntry.this.getContrastedColor( - context, isLowPriority, backgroundColor); - } - - @Override - public boolean canPeek() { - return isStickyAndNotDemoted(); - } - - @Override - public long getWhen() { - return getSbn().getNotification().getWhen(); - } - - @Override - public IconPack getIcons() { - return NotificationEntry.this.getIcons(); - } - - @Override - public boolean isColorized() { - return getSbn().getNotification().isColorized(); - } - - @Override - @Nullable - public StatusBarNotification getSbn() { - return NotificationEntry.this.getSbn(); - } - - @Override - public boolean canDragAndDrop() { - boolean canBubble = canBubble(); - Notification notif = getSbn().getNotification(); - PendingIntent dragIntent = notif.contentIntent != null ? notif.contentIntent - : notif.fullScreenIntent; - if (dragIntent != null && dragIntent.isActivity() && !canBubble) { - return true; - } - return false; - } - - @Override - public boolean isBubbleCapable() { - return NotificationEntry.this.isBubble(); - } - - @Override - @Nullable - public String getStyle() { - return getNotificationStyle(); - } - - @Override - public int getSectionBucket() { - return mBucket; - } - - @Override - public boolean isAmbient() { - return mRanking.isAmbient(); - } - - @Override - public boolean isFullScreenCapable() { - return getSbn().getNotification().fullScreenIntent != null; - } - - } - - public EntryAdapter getEntryAdapter() { - return mEntryAdapter; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt new file mode 100644 index 000000000000..0ff2dd7c7f43 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt @@ -0,0 +1,145 @@ +/* + * 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.notification.collection + +import android.content.Context +import android.service.notification.StatusBarNotification +import com.android.internal.logging.MetricsLogger +import com.android.systemui.statusbar.notification.NotificationActivityStarter +import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator +import com.android.systemui.statusbar.notification.icon.IconPack +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider +import kotlinx.coroutines.flow.StateFlow + +class NotificationEntryAdapter( + private val notificationActivityStarter: NotificationActivityStarter, + private val metricsLogger: MetricsLogger, + private val peopleNotificationIdentifier: PeopleNotificationIdentifier, + private val iconStyleProvider: NotificationIconStyleProvider, + private val visualStabilityCoordinator: VisualStabilityCoordinator, + private val entry: NotificationEntry, +) : EntryAdapter { + + override fun getParent(): PipelineEntry? { + return entry.parent + } + + override fun isTopLevelEntry(): Boolean { + return parent != null && + (parent === GroupEntry.ROOT_ENTRY || BundleEntry.ROOT_BUNDLES.contains(parent)) + } + + override fun getKey(): String { + return entry.key + } + + override fun getRow(): ExpandableNotificationRow { + return entry.row + } + + override fun isGroupRoot(): Boolean { + if (isTopLevelEntry || parent == null) { + return false + } + return (entry.parent as? GroupEntry)?.summary == entry + } + + override fun isSensitive(): StateFlow<Boolean> { + return entry.isSensitive + } + + override fun isClearable(): Boolean { + return entry.isClearable + } + + override fun getTargetSdk(): Int { + return entry.targetSdk + } + + override fun getSummarization(): String? { + return entry.ranking?.summarization + } + + override fun prepareForInflation() { + entry.sbn.clearPackageContext() + } + + override fun getContrastedColor( + context: Context?, + isLowPriority: Boolean, + backgroundColor: Int, + ): Int { + return entry.getContrastedColor(context, isLowPriority, backgroundColor) + } + + override fun canPeek(): Boolean { + return entry.isStickyAndNotDemoted + } + + override fun getWhen(): Long { + return entry.sbn.notification.getWhen() + } + + override fun getIcons(): IconPack { + return entry.icons + } + + override fun isColorized(): Boolean { + return entry.sbn.notification.isColorized + } + + override fun getSbn(): StatusBarNotification { + return entry.sbn + } + + override fun canDragAndDrop(): Boolean { + val canBubble: Boolean = entry.canBubble() + val notif = entry.sbn.notification + val dragIntent = + if (notif.contentIntent != null) notif.contentIntent else notif.fullScreenIntent + if (dragIntent != null && dragIntent.isActivity && !canBubble) { + return true + } + return false + } + + override fun isBubbleCapable(): Boolean { + return entry.isBubble + } + + override fun getStyle(): String? { + return entry.notificationStyle + } + + override fun getSectionBucket(): Int { + return entry.bucket + } + + override fun isAmbient(): Boolean { + return entry.ranking.isAmbient + } + + override fun isFullScreenCapable(): Boolean { + return entry.sbn.notification.fullScreenIntent != null + } + + override fun onNotificationBubbleIconClicked() { + notificationActivityStarter.onNotificationBubbleIconClicked(entry) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index f6e66237d438..fdb8cd871dd9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -27,10 +27,10 @@ import com.android.systemui.statusbar.chips.notification.domain.interactor.Statu import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.NotifPipelineFlags import com.android.systemui.statusbar.notification.collection.GroupEntry -import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.PipelineEntry import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter @@ -459,7 +459,12 @@ constructor( } else { if (posted.isHeadsUpEntry) { // We don't want this to be interrupting anymore, let's remove it - hunMutator.removeNotification(posted.key, false /*removeImmediately*/) + // If the notification is pinned by the user, the only way a user can un-pin + // it is by tapping the status bar notification chip. Since that's a clear + // user action, we should remove the HUN immediately instead of waiting for + // any sort of minimum timeout. + val shouldRemoveImmediately = posted.isPinnedByUser + hunMutator.removeNotification(posted.key, shouldRemoveImmediately) } else { // Don't let the bind finish cancelHeadsUpBind(posted.entry) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StabilizeHeadsUpGroup.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StabilizeHeadsUpGroup.kt index 179a87d7f007..cf7881214ddf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StabilizeHeadsUpGroup.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StabilizeHeadsUpGroup.kt @@ -50,7 +50,9 @@ object StabilizeHeadsUpGroup { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 53d5dbc58677..ef3da9498f70 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -37,6 +37,8 @@ import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider; import com.android.systemui.statusbar.notification.VisibilityLocationProvider; +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactory; +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactoryImpl; import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl; import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore; import com.android.systemui.statusbar.notification.collection.NotifLiveDataStoreImpl; @@ -340,4 +342,8 @@ public interface NotificationsModule { return MagneticNotificationRowManager.getEmpty(); } } + + /** Provides an instance of {@link EntryAdapterFactory} */ + @Binds + EntryAdapterFactory provideEntryAdapterFactory(EntryAdapterFactoryImpl impl); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt index d3a44f9b1497..52bf894c796c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt @@ -50,7 +50,9 @@ object ModesEmptyShadeFix { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/NotifRedesignFooter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/NotifRedesignFooter.kt index dbe046fdf30b..90faa4d2c714 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/NotifRedesignFooter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/NotifRedesignFooter.kt @@ -50,7 +50,9 @@ object NotifRedesignFooter { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt index 8eca16622084..c401d8212c29 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/AvalancheController.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.headsup import android.os.Handler -import android.util.Log import androidx.annotation.VisibleForTesting import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger @@ -24,9 +23,9 @@ import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun -import com.android.systemui.util.Compile import java.io.PrintWriter import javax.inject.Inject @@ -155,6 +154,7 @@ constructor( } else if (entry in nextMap) { outcome = "update next" nextMap[entry]?.add(runnable) + checkNextPinnedByUser(entry)?.let { outcome = "$outcome & $it" } } else if (headsUpEntryShowing == null) { outcome = "show now" showNow(entry, arrayListOf(runnable)) @@ -166,17 +166,22 @@ constructor( outcome = "add next" addToNext(entry, runnable) - // Shorten headsUpEntryShowing display time - val nextIndex = nextList.indexOf(entry) - val isOnlyNextEntry = nextIndex == 0 && nextList.size == 1 - if (isOnlyNextEntry) { - // HeadsUpEntry.updateEntry recursively calls AvalancheController#update - // and goes to the isShowing case above - headsUpEntryShowing!!.updateEntry( - /* updatePostTime= */ false, - /* updateEarliestRemovalTime= */ false, - /* reason= */ "shorten duration of previously-last HUN", - ) + val nextIsPinnedByUserResult = checkNextPinnedByUser(entry) + if (nextIsPinnedByUserResult != null) { + outcome = "$outcome & $nextIsPinnedByUserResult" + } else { + // Shorten headsUpEntryShowing display time + val nextIndex = nextList.indexOf(entry) + val isOnlyNextEntry = nextIndex == 0 && nextList.size == 1 + if (isOnlyNextEntry) { + // HeadsUpEntry.updateEntry recursively calls AvalancheController#update + // and goes to the isShowing case above + headsUpEntryShowing!!.updateEntry( + /* updatePostTime= */ false, + /* updateEarliestRemovalTime= */ false, + /* reason= */ "shorten duration of previously-last HUN", + ) + } } } outcome += getStateStr() @@ -190,6 +195,28 @@ constructor( } /** + * Checks if the given entry is requesting [PinnedStatus.PinnedByUser] status and makes the + * correct updates if needed. + * + * @return a string representing the outcome, or null if nothing changed. + */ + private fun checkNextPinnedByUser(entry: HeadsUpEntry): String? { + if ( + StatusBarNotifChips.isEnabled && + entry.requestedPinnedStatus == PinnedStatus.PinnedByUser + ) { + val string = "next is PinnedByUser" + headsUpEntryShowing?.updateEntry( + /* updatePostTime= */ false, + /* updateEarliestRemovalTime= */ false, + /* reason= */ string, + ) + return string + } + return null + } + + /** * Run or ignore Runnable for given HeadsUpEntry. If entry was never shown, ignore and delete * all Runnables associated with that entry. */ @@ -243,19 +270,22 @@ constructor( outcome = "remove showing. ${getStateStr()}" } else { runnable.run() - outcome = "run runnable for untracked HUN " + + outcome = + "run runnable for untracked HUN " + "(was dropped or shown when AC was disabled). ${getStateStr()}" } headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome) } /** - * Returns duration based on + * Returns how much longer the given entry should show based on: * 1) Whether HeadsUpEntry is the last one tracked by AvalancheController - * 2) The priority of the top HUN in the next batch Used by - * BaseHeadsUpManager.HeadsUpEntry.calculateFinishTime to shorten display duration. + * 2) The priority of the top HUN in the next batch + * + * Used by [HeadsUpManagerImpl.HeadsUpEntry]'s finishTimeCalculator to shorten display duration. */ - fun getDurationMs(entry: HeadsUpEntry?, autoDismissMs: Int): Int { + fun getDuration(entry: HeadsUpEntry?, autoDismissMsValue: Int): RemainingDuration { + val autoDismissMs = RemainingDuration.UpdatedDuration(autoDismissMsValue) if (!isEnabled()) { // Use default duration, like we did before AvalancheController existed return autoDismissMs @@ -273,7 +303,11 @@ constructor( val thisKey = getKey(entry) if (entryList.isEmpty()) { headsUpManagerLogger.logAvalancheDuration( - thisKey, autoDismissMs, "No avalanche HUNs, use default", nextKey = "") + thisKey, + autoDismissMs, + "No avalanche HUNs, use default", + nextKey = "", + ) return autoDismissMs } // entryList.indexOf(entry) returns -1 even when the entry is in entryList @@ -285,28 +319,64 @@ constructor( } if (thisEntryIndex == -1) { headsUpManagerLogger.logAvalancheDuration( - thisKey, autoDismissMs, "Untracked entry, use default", nextKey = "") + thisKey, + autoDismissMs, + "Untracked entry, use default", + nextKey = "", + ) return autoDismissMs } val nextEntryIndex = thisEntryIndex + 1 if (nextEntryIndex >= entryList.size) { headsUpManagerLogger.logAvalancheDuration( - thisKey, autoDismissMs, "Last entry, use default", nextKey = "") + thisKey, + autoDismissMs, + "Last entry, use default", + nextKey = "", + ) return autoDismissMs } val nextEntry = entryList[nextEntryIndex] val nextKey = getKey(nextEntry) + + if ( + StatusBarNotifChips.isEnabled && + nextEntry.requestedPinnedStatus == PinnedStatus.PinnedByUser + ) { + return RemainingDuration.HideImmediately.also { + headsUpManagerLogger.logAvalancheDuration( + thisKey, + duration = it, + "next is PinnedByUser", + nextKey, + ) + } + } if (nextEntry.compareNonTimeFields(entry) == -1) { - headsUpManagerLogger.logAvalancheDuration( - thisKey, 500, "LOWER priority than next: ", nextKey) - return 500 + return RemainingDuration.UpdatedDuration(500).also { + headsUpManagerLogger.logAvalancheDuration( + thisKey, + duration = it, + "LOWER priority than next: ", + nextKey, + ) + } } else if (nextEntry.compareNonTimeFields(entry) == 0) { - headsUpManagerLogger.logAvalancheDuration( - thisKey, 1000, "SAME priority as next: ", nextKey) - return 1000 + return RemainingDuration.UpdatedDuration(1000).also { + headsUpManagerLogger.logAvalancheDuration( + thisKey, + duration = it, + "SAME priority as next: ", + nextKey, + ) + } } else { headsUpManagerLogger.logAvalancheDuration( - thisKey, autoDismissMs, "HIGHER priority than next: ", nextKey) + thisKey, + autoDismissMs, + "HIGHER priority than next: ", + nextKey, + ) return autoDismissMs } } @@ -377,11 +447,11 @@ constructor( } private fun showNext() { - headsUpManagerLogger.logAvalancheStage("show next", key = "") + headsUpManagerLogger.logAvalancheStage("show next", key = "") headsUpEntryShowing = null if (nextList.isEmpty()) { - headsUpManagerLogger.logAvalancheStage("no more", key = "") + headsUpManagerLogger.logAvalancheStage("no more", key = "") previousHunKey = "" return } @@ -432,10 +502,12 @@ constructor( private fun getStateStr(): String { return "\n[AC state]" + - "\nshow: ${getKey(headsUpEntryShowing)}" + - "\nprevious: $previousHunKey" + - "\n$nextStr" + - "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " + baseEntryMapStr() + "\n" + "\nshow: ${getKey(headsUpEntryShowing)}" + + "\nprevious: $previousHunKey" + + "\n$nextStr" + + "\n[HeadsUpManagerImpl.mHeadsUpEntryMap] " + + baseEntryMapStr() + + "\n" } private val nextStr: String @@ -447,7 +519,7 @@ constructor( // This should never happen val nextMapStr = nextMap.keys.joinToString("\n ") { getKey(it) } return "next list (${nextList.size}):\n $nextListStr" + - "\nnext map (${nextMap.size}):\n $nextMapStr" + "\nnext map (${nextMap.size}):\n $nextMapStr" } fun getKey(entry: HeadsUpEntry?): String { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimationEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimationEvent.kt new file mode 100644 index 000000000000..ab8489653f50 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimationEvent.kt @@ -0,0 +1,32 @@ +/* + * 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.notification.headsup + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow + +/** Models the data needed for a heads-up notification animation. */ +data class HeadsUpAnimationEvent( + /** The row corresponding to the heads-up notification. */ + val row: ExpandableNotificationRow, + /** + * True if this notification should do a appearance animation, false if this notification should + * do a disappear animation. + */ + val isHeadsUpAppearance: Boolean, + /** True if the status bar is showing a chip corresponding to this notification. */ + val hasStatusBarChip: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimator.kt index 177574f57c1c..2f2d80a46f60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpAnimator.kt @@ -17,13 +17,18 @@ package com.android.systemui.statusbar.notification.headsup import android.content.Context +import com.android.internal.policy.SystemBarUtils import com.android.systemui.res.R +import com.android.systemui.statusbar.ui.SystemBarUtilsProxy /** * A class shared between [StackScrollAlgorithm] and [StackStateAnimator] to ensure all heads up * animations use the same animation values. + * + * @param systemBarUtilsProxy optional utility class to provide the status bar height. Typically + * null in production code and non-null in tests. */ -class HeadsUpAnimator(context: Context) { +class HeadsUpAnimator(context: Context, private val systemBarUtilsProxy: SystemBarUtilsProxy?) { init { NotificationsHunSharedAnimationValues.unsafeAssertInNewMode() } @@ -32,6 +37,7 @@ class HeadsUpAnimator(context: Context) { var stackTopMargin: Int = 0 private var headsUpAppearStartAboveScreen = context.fetchHeadsUpAppearStartAboveScreen() + private var statusBarHeight = fetchStatusBarHeight(context) /** * Returns the Y translation for a heads-up notification animation. @@ -40,14 +46,20 @@ class HeadsUpAnimator(context: Context) { * animation. For a disappear animation, the returned Y translation should be the ending value * of the animation. */ - fun getHeadsUpYTranslation(isHeadsUpFromBottom: Boolean): Int { - NotificationsHunSharedAnimationValues.unsafeAssertInNewMode() + fun getHeadsUpYTranslation(isHeadsUpFromBottom: Boolean, hasStatusBarChip: Boolean): Int { + if (NotificationsHunSharedAnimationValues.isUnexpectedlyInLegacyMode()) return 0 if (isHeadsUpFromBottom) { // start from or end at the bottom of the screen return headsUpAppearHeightBottom + headsUpAppearStartAboveScreen } + if (hasStatusBarChip) { + // If this notification is also represented by a chip in the status bar, we don't want + // any HUN transitions to obscure that chip. + return statusBarHeight - stackTopMargin + } + // start from or end at the top of the screen return -stackTopMargin - headsUpAppearStartAboveScreen } @@ -55,9 +67,15 @@ class HeadsUpAnimator(context: Context) { /** Should be invoked when resource values may have changed. */ fun updateResources(context: Context) { headsUpAppearStartAboveScreen = context.fetchHeadsUpAppearStartAboveScreen() + statusBarHeight = fetchStatusBarHeight(context) } private fun Context.fetchHeadsUpAppearStartAboveScreen(): Int { return this.resources.getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen) } + + private fun fetchStatusBarHeight(context: Context): Int { + return systemBarUtilsProxy?.getStatusBarHeight() + ?: SystemBarUtils.getStatusBarHeight(context) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index d16ad80ca8b9..ca94655318b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -46,7 +46,6 @@ import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; -import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; @@ -320,15 +319,17 @@ public class HeadsUpManagerImpl mLogger.logShowNotificationRequest(entry, isPinnedByUser); + PinnedStatus requestedPinnedStatus = + isPinnedByUser + ? PinnedStatus.PinnedByUser + : PinnedStatus.PinnedBySystem; + headsUpEntry.setRequestedPinnedStatus(requestedPinnedStatus); + Runnable runnable = () -> { mLogger.logShowNotification(entry, isPinnedByUser); // Add new entry and begin managing it mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry); - PinnedStatus requestedPinnedStatus = - isPinnedByUser - ? PinnedStatus.PinnedByUser - : PinnedStatus.PinnedBySystem; onEntryAdded(headsUpEntry, requestedPinnedStatus); // TODO(b/328390331) move accessibility events to the view layer entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); @@ -1289,10 +1290,17 @@ public class HeadsUpManagerImpl @Nullable private Runnable mCancelRemoveRunnable; private boolean mGutsShownPinned; + /** The *current* pinned status of this HUN. */ private final MutableStateFlow<PinnedStatus> mPinnedStatus = StateFlowKt.MutableStateFlow(PinnedStatus.NotPinned); /** + * The *requested* pinned status of this HUN. {@link AvalancheController} uses this value to + * know if the current HUN needs to be removed so that a pinned-by-user HUN can show. + */ + private PinnedStatus mRequestedPinnedStatus = PinnedStatus.NotPinned; + + /** * If the time this entry has been on was extended */ private boolean extended; @@ -1352,6 +1360,20 @@ public class HeadsUpManagerImpl } } + /** Sets what pinned status this HUN is requesting. */ + void setRequestedPinnedStatus(PinnedStatus pinnedStatus) { + if (!StatusBarNotifChips.isEnabled() && pinnedStatus == PinnedStatus.PinnedByUser) { + Log.w(TAG, "PinnedByUser status not allowed if StatusBarNotifChips is disabled"); + mRequestedPinnedStatus = PinnedStatus.NotPinned; + } else { + mRequestedPinnedStatus = pinnedStatus; + } + } + + PinnedStatus getRequestedPinnedStatus() { + return mRequestedPinnedStatus; + } + @VisibleForTesting void setRowPinnedStatus(PinnedStatus pinnedStatus) { if (mEntry != null) mEntry.setRowPinnedStatus(pinnedStatus); @@ -1410,11 +1432,29 @@ public class HeadsUpManagerImpl } FinishTimeUpdater finishTimeCalculator = () -> { - final long finishTime = calculateFinishTime(); + RemainingDuration remainingDuration = + mAvalancheController.getDuration(this, mAutoDismissTime); + + if (remainingDuration instanceof RemainingDuration.HideImmediately) { + /* Check if */ StatusBarNotifChips.isUnexpectedlyInLegacyMode(); + return 0; + } + + int remainingTimeoutMs; + if (isStickyForSomeTime()) { + remainingTimeoutMs = mStickyForSomeTimeAutoDismissTime; + } else { + remainingTimeoutMs = + ((RemainingDuration.UpdatedDuration) remainingDuration).getDuration(); + } + final long duration = getRecommendedHeadsUpTimeoutMs(remainingTimeoutMs); + final long timeoutTimestamp = + mPostTime + duration + (extended ? mExtensionTime : 0); + final long now = mSystemClock.elapsedRealtime(); return NotificationThrottleHun.isEnabled() - ? Math.max(finishTime, mEarliestRemovalTime) - now - : Math.max(finishTime - now, mMinimumDisplayTimeDefault); + ? Math.max(timeoutTimestamp, mEarliestRemovalTime) - now + : Math.max(timeoutTimestamp - now, mMinimumDisplayTimeDefault); }; scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); @@ -1696,21 +1736,6 @@ public class HeadsUpManagerImpl } /** - * @return When the notification should auto-dismiss itself, based on - * {@link SystemClock#elapsedRealtime()} - */ - private long calculateFinishTime() { - int requestedTimeOutMs; - if (isStickyForSomeTime()) { - requestedTimeOutMs = mStickyForSomeTimeAutoDismissTime; - } else { - requestedTimeOutMs = mAvalancheController.getDurationMs(this, mAutoDismissTime); - } - final long duration = getRecommendedHeadsUpTimeoutMs(requestedTimeOutMs); - return mPostTime + duration + (extended ? mExtensionTime : 0); - } - - /** * Get user-preferred or default timeout duration. The larger one will be returned. * @return milliseconds before auto-dismiss */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt index 388d357b3b15..00b05cbd7bec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerLogger.kt @@ -106,13 +106,23 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { ) } - fun logAvalancheDuration(thisKey: String, duration: Int, reason: String, nextKey: String) { + fun logAvalancheDuration( + thisKey: String, + duration: RemainingDuration, + reason: String, + nextKey: String, + ) { + val durationMs = + when (duration) { + is RemainingDuration.UpdatedDuration -> duration.duration + is RemainingDuration.HideImmediately -> 0 + } buffer.log( TAG, INFO, { str1 = thisKey - int1 = duration + int1 = durationMs str2 = reason str3 = nextKey }, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/NotificationsHunSharedAnimationValues.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/NotificationsHunSharedAnimationValues.kt index c53831671bfc..f52d351cfb0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/NotificationsHunSharedAnimationValues.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/NotificationsHunSharedAnimationValues.kt @@ -50,7 +50,9 @@ object NotificationsHunSharedAnimationValues { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/RemainingDuration.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/RemainingDuration.kt new file mode 100644 index 000000000000..fd7f4e87e8e8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/RemainingDuration.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.statusbar.notification.headsup + +/** Models how much longer a HUN should be displayed. */ +sealed interface RemainingDuration { + /** This HUN should be hidden immediately, regardless of any minimum time enforcements. */ + data object HideImmediately : RemainingDuration + + /** This HUN should hide after [duration] milliseconds have occurred. */ + data class UpdatedDuration(val duration: Int) : RemainingDuration +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt index aa81ebf22ac6..147a5afea306 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt @@ -18,11 +18,10 @@ package com.android.systemui.statusbar.notification.icon.ui.viewbinder import android.view.Display import androidx.lifecycle.lifecycleScope -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.traceSection import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.display.data.repository.PerDisplayRepository import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel @@ -37,7 +36,8 @@ class NotificationIconContainerStatusBarViewBinder @Inject constructor( private val viewModel: NotificationIconContainerStatusBarViewModel, - @ShadeDisplayAware private val configuration: ConfigurationState, + private val configurationStateRepository: PerDisplayRepository<ConfigurationState>, + private val defaultConfigurationState: ConfigurationState, private val systemBarUtilsState: SystemBarUtilsState, private val failureTracker: StatusBarIconViewBindingFailureTracker, private val defaultDisplayViewStore: StatusBarNotificationIconViewStore, @@ -56,12 +56,14 @@ constructor( lifecycleScope.launch { it.activate() } } } + val configurationState: ConfigurationState = + configurationStateRepository[displayId] ?: defaultConfigurationState lifecycleScope.launch { NotificationIconContainerViewBinder.bind( displayId = displayId, view = view, viewModel = viewModel, - configuration = configuration, + configuration = configurationState, systemBarUtilsState = systemBarUtilsState, failureTracker = failureTracker, viewStore = viewStore, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUi.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUi.kt index 5bf5766a886c..da59a40a1624 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUi.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUi.kt @@ -50,7 +50,9 @@ object PromotedNotificationUi { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt index 8679975998f2..c6e3da1c5750 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiAod.kt @@ -49,7 +49,9 @@ object PromotedNotificationUiAod { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt index 287e00257bc5..adeddde8ccc3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationUiForceExpanded.kt @@ -50,7 +50,9 @@ object PromotedNotificationUiForceExpanded { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if 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 66929a579eca..76d285007d1c 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 @@ -99,6 +99,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarIconView; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.FeedbackIcon; @@ -110,6 +111,8 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntryAdapter; +import com.android.systemui.statusbar.notification.collection.PipelineEntry; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; @@ -120,6 +123,7 @@ import com.android.systemui.statusbar.notification.people.PeopleNotificationIden import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModelImpl; import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss; @@ -179,6 +183,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mIsFaded; private boolean mIsPromotedOngoing = false; + private boolean mHasStatusBarChipDuringHeadsUpAnimation = false; @Nullable public ImageModelIndex mImageModelIndex = null; @@ -426,7 +431,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView onExpansionChanged(true /* userAction */, wasExpanded); } } else if (mEnableNonGroupedNotificationExpand) { - if (v.isAccessibilityFocused()) { + if (v != null && v.isAccessibilityFocused()) { mPrivateLayout.setFocusOnVisibilityChange(); } boolean nowExpanded; @@ -1816,6 +1821,22 @@ public class ExpandableNotificationRow extends ActivatableNotificationView ); } + /** + * Init the bundle header view. The ComposeView is initialized within with the passed viewModel. + * This can only be init once and not in conjunction with any other header view. + */ + public void initBundleHeader(@NonNull BundleHeaderViewModelImpl bundleHeaderViewModel) { + if (NotificationBundleUi.isUnexpectedlyInLegacyMode()) return; + NotificationChildrenContainer childrenContainer = getChildrenContainerNonNull(); + bundleHeaderViewModel.setOnExpandClickListener(mExpandClickListener); + + childrenContainer.initBundleHeader(bundleHeaderViewModel); + + if (TransparentHeaderFix.isEnabled()) { + updateBackgroundForGroupState(); + } + } + public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { boolean wasAboveShelf = isAboveShelf(); boolean changed = headsUpAnimatingAway != mHeadsupDisappearRunning; @@ -2122,7 +2143,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * Initialize row. */ public void initialize( - NotificationEntry entry, + EntryAdapter entryAdapter, + PipelineEntry entry, RemoteInputViewSubcomponent.Factory rivSubcomponentFactory, String appName, @NonNull String notificationKey, @@ -2150,11 +2172,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView NotificationRebindingTracker notificationRebindingTracker) { if (NotificationBundleUi.isEnabled()) { + mEntryAdapter = entryAdapter; // TODO (b/395857098): remove when all usages are migrated - mEntryAdapter = entry.getEntryAdapter(); - mEntry = entry; + mEntry = (NotificationEntry) entry; } else { - mEntry = entry; + mEntry = (NotificationEntry) entry; } mAppName = appName; mRebindingTracker = notificationRebindingTracker; @@ -2943,6 +2965,30 @@ public class ExpandableNotificationRow extends ActivatableNotificationView setExpandable(!mIsPromotedOngoing); } + /** + * Sets whether the status bar is showing a chip corresponding to this notification. + * + * Only set when this notification's heads-up status changes since that's the only time it's + * relevant. + */ + public void setHasStatusBarChipDuringHeadsUpAnimation(boolean hasStatusBarChip) { + if (StatusBarNotifChips.isUnexpectedlyInLegacyMode()) { + return; + } + mHasStatusBarChipDuringHeadsUpAnimation = hasStatusBarChip; + } + + /** + * Returns true if the status bar is showing a chip corresponding to this notification during a + * heads-up appear or disappear animation. + * + * Note that this value is only set when this notification's heads-up status changes since + * that's the only time it's relevant. + */ + public boolean hasStatusBarChipDuringHeadsUpAnimation() { + return StatusBarNotifChips.isEnabled() && mHasStatusBarChipDuringHeadsUpAnimation; + } + @Override public void setClipToActualHeight(boolean clipToActualHeight) { super.setClipToActualHeight(clipToActualHeight || isUserLocked()); @@ -3022,7 +3068,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mUserLocked = userLocked; mPrivateLayout.setUserExpanding(userLocked); - mPublicLayout.setUserExpanding(userLocked); // This is intentionally not guarded with mIsSummaryWithChildren since we might have had // children but not anymore. if (mChildrenContainer != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 02e8f4917da4..ac55930f5c11 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -45,7 +45,11 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.FeedbackIcon; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.NotificationActivityStarter; +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactory; +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactoryImpl; +import com.android.systemui.statusbar.notification.collection.PipelineEntry; +import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; @@ -56,6 +60,7 @@ import com.android.systemui.statusbar.notification.people.PeopleNotificationIden import com.android.systemui.statusbar.notification.row.dagger.AppName; import com.android.systemui.statusbar.notification.row.dagger.NotificationKey; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -118,8 +123,8 @@ public class ExpandableNotificationRowController implements NotifViewController private final IStatusBarService mStatusBarService; private final UiEventLogger mUiEventLogger; private final MSDLPlayer mMSDLPlayer; - private final NotificationSettingsController mSettingsController; + private final EntryAdapterFactory mEntryAdapterFactory; @VisibleForTesting final NotificationSettingsController.Listener mSettingsListener = @@ -285,7 +290,8 @@ public class ExpandableNotificationRowController implements NotifViewController IStatusBarService statusBarService, UiEventLogger uiEventLogger, MSDLPlayer msdlPlayer, - NotificationRebindingTracker notificationRebindingTracker) { + NotificationRebindingTracker notificationRebindingTracker, + EntryAdapterFactory entryAdapterFactory) { mView = view; mListContainer = listContainer; mRemoteInputViewSubcomponentFactory = rivSubcomponentFactory; @@ -322,14 +328,16 @@ public class ExpandableNotificationRowController implements NotifViewController mStatusBarService = statusBarService; mUiEventLogger = uiEventLogger; mMSDLPlayer = msdlPlayer; + mEntryAdapterFactory = entryAdapterFactory; } /** * Initialize the controller. */ - public void init(NotificationEntry entry) { + public void init(PipelineEntry entry) { mActivatableNotificationViewController.init(); mView.initialize( + mEntryAdapterFactory.create(entry), entry, mRemoteInputViewSubcomponentFactory, mAppName, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt index 77135802eced..d02f636728fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt @@ -42,14 +42,6 @@ class MagicActionBackgroundDrawable( private val buttonShape = Path() // Color and style - private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - val bgColor = - context.getColor( - com.android.internal.R.color.materialColorPrimaryContainer - ) - color = bgColor - style = Paint.Style.FILL - } private val outlinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { val outlineColor = context.getColor( @@ -99,7 +91,6 @@ class MagicActionBackgroundDrawable( canvas.save() // Draw background canvas.clipPath(buttonShape) - canvas.drawPath(buttonShape, bgPaint) // Apply gradient to outline canvas.drawPath(buttonShape, outlinePaint) updateGradient(boundsF) @@ -128,13 +119,11 @@ class MagicActionBackgroundDrawable( } override fun setAlpha(alpha: Int) { - bgPaint.alpha = alpha outlinePaint.alpha = alpha invalidateSelf() } override fun setColorFilter(colorFilter: ColorFilter?) { - bgPaint.colorFilter = colorFilter outlinePaint.colorFilter = colorFilter invalidateSelf() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java index 4146a941c025..8176d2a1be44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java @@ -211,14 +211,28 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp rightIconLP.setMarginEnd(horizontalMargin); mRightIcon.setLayoutParams(rightIconLP); + // if there is no title and topline view, there is nothing to adjust. + if (mNotificationTopLine == null && mTitle == null) { + return; + } + // align top line view to start of the right icon. final int iconSize = mView.getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_right_icon_size); final int marginEnd = 2 * horizontalMargin + iconSize; - mNotificationTopLine.setHeaderTextMarginEnd(marginEnd); + final boolean isTitleInTopLine; + // set margin end for the top line view if it exists + if (mNotificationTopLine != null) { + mNotificationTopLine.setHeaderTextMarginEnd(marginEnd); + isTitleInTopLine = mNotificationTopLine.isTitlePresent(); + } else { + isTitleInTopLine = false; + } + // Margin is to be applied to the title only when it is in the body, + // but not in the title. // title has too much margin on the right, so we need to reduce it - if (mTitle != null) { + if (!isTitleInTopLine && mTitle != null) { final ViewGroup.MarginLayoutParams titleLP = (ViewGroup.MarginLayoutParams) mTitle.getLayoutParams(); titleLP.setMarginEnd(marginEnd); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationBundleUi.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationBundleUi.kt index 11c942c9bcc0..37212a387fad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationBundleUi.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationBundleUi.kt @@ -49,7 +49,9 @@ object NotificationBundleUi { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if @@ -57,4 +59,4 @@ object NotificationBundleUi { */ @JvmStatic inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -}
\ No newline at end of file +} 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 e830d18b7d73..315d37e55bc3 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 @@ -39,9 +39,11 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.compose.ui.platform.ComposeView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.NotificationExpandButton; +import com.android.systemui.notifications.ui.composable.row.BundleHeaderKt; import com.android.systemui.res.R; import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.CrossFadeHelper; @@ -58,6 +60,7 @@ import com.android.systemui.statusbar.notification.row.HybridGroupManager; import com.android.systemui.statusbar.notification.row.HybridNotificationView; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderViewModelImpl; import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; @@ -119,6 +122,13 @@ public class NotificationChildrenContainer extends ViewGroup */ private boolean mEnableShadowOnChildNotifications; + /** + * This view is only set when this NCC is a bundle. If this view is set, all other header + * view variants have to be null. + */ + private ComposeView mBundleHeaderView; + private BundleHeaderViewModelImpl mBundleHeaderViewModel; + private NotificationHeaderView mGroupHeader; private NotificationHeaderViewWrapper mGroupHeaderWrapper; private NotificationHeaderView mMinimizedGroupHeader; @@ -189,6 +199,9 @@ public class NotificationChildrenContainer extends ViewGroup R.dimen.notification_children_container_top_padding); mHeaderHeight = mCollapsedHeaderMargin + mAdditionalExpandedHeaderMargin; } + if (mBundleHeaderView != null) { + initBundleDimens(); + } mCollapsedBottomPadding = res.getDimensionPixelOffset( R.dimen.notification_children_collapsed_bottom_padding); mEnableShadowOnChildNotifications = @@ -244,6 +257,10 @@ public class NotificationChildrenContainer extends ViewGroup mMinimizedGroupHeader.getMeasuredWidth(), mMinimizedGroupHeader.getMeasuredHeight()); } + if (mBundleHeaderView != null) { + mBundleHeaderView.layout(0, 0, mBundleHeaderView.getMeasuredWidth(), + mBundleHeaderView.getMeasuredHeight()); + } } @Override @@ -295,6 +312,9 @@ public class NotificationChildrenContainer extends ViewGroup if (mMinimizedGroupHeader != null) { mMinimizedGroupHeader.measure(widthMeasureSpec, headerHeightSpec); } + if (mBundleHeaderView != null) { + mBundleHeaderView.measure(widthMeasureSpec, headerHeightSpec); + } setMeasuredDimension(width, height); Trace.endSection(); @@ -489,6 +509,28 @@ public class NotificationChildrenContainer extends ViewGroup } /** + * Init the bundle header view. The ComposeView is initialized within with the passed viewModel. + * This can only be init once and not in conjunction with any other header view. + */ + public void initBundleHeader(@NonNull BundleHeaderViewModelImpl viewModel) { + if (NotificationBundleUi.isUnexpectedlyInLegacyMode()) return; + if (mBundleHeaderView != null) return; + initBundleDimens(); + + mBundleHeaderViewModel = viewModel; + mBundleHeaderView = BundleHeaderKt.createComposeView(mBundleHeaderViewModel, getContext()); + addView(mBundleHeaderView); + invalidate(); + } + + private void initBundleDimens() { + NotificationBundleUi.unsafeAssertInNewMode(); + mCollapsedHeaderMargin = mHeaderHeight; + mAdditionalExpandedHeaderMargin = 0; + mCollapsedBottomPadding = 0; + } + + /** * Set the group header view * @param headerView view to set * @param onClickListener OnClickListener of the header view @@ -1311,6 +1353,17 @@ public class NotificationChildrenContainer extends ViewGroup mGroupHeader.setHeaderBackgroundDrawable(null); } } + if (mBundleHeaderView != null) { + if (expanded) { + ColorDrawable cd = new ColorDrawable(); + cd.setColor(mContainingNotification.calculateBgColor()); + // TODO(b/389839492): The backgroundDrawable needs an outline like in the original: + // setOutlineProvider(mProvider); + mBundleHeaderViewModel.setBackgroundDrawable(cd); + } else { + mBundleHeaderViewModel.setBackgroundDrawable(null); + } + } } public int getMaxContentHeight() { 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 1a17b8efb4ae..c2c271bd6e81 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 @@ -104,6 +104,7 @@ import com.android.systemui.shade.QSHeaderBoundsProvider; import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.FakeShadowView; @@ -117,6 +118,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView; import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; +import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimationEvent; import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator; import com.android.systemui.statusbar.notification.headsup.HeadsUpTouchHelper; import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; @@ -139,6 +141,7 @@ import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScr import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.policy.ScrollAdapter; import com.android.systemui.statusbar.policy.SplitShadeStateController; +import com.android.systemui.statusbar.ui.SystemBarUtilsProxy; import com.android.systemui.util.Assert; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.DumpUtilsKt; @@ -351,10 +354,11 @@ public class NotificationStackScrollLayout private final int[] mTempInt2 = new int[2]; private final HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>(); private final HashSet<ExpandableView> mClearTransientViewsWhenFinished = new HashSet<>(); - private final HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations - = new HashSet<>(); + private final Map<ExpandableNotificationRow, HeadsUpAnimationEvent> mHeadsUpChangeAnimations + = new HashMap<>(); private boolean mForceNoOverlappingRendering; - private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>(); + private final ArrayList<ExpandableNotificationRow> mTmpHeadsUpChangeAnimations = + new ArrayList<>(); private boolean mAnimationRunning; private final ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater = new ViewTreeObserver.OnPreDrawListener() { @@ -673,7 +677,7 @@ public class NotificationStackScrollLayout mExpandHelper.setScrollAdapter(mScrollAdapter); if (NotificationsHunSharedAnimationValues.isEnabled()) { - mHeadsUpAnimator = new HeadsUpAnimator(context); + mHeadsUpAnimator = new HeadsUpAnimator(context, /* systemBarUtilsProxy= */ null); } else { mHeadsUpAnimator = null; } @@ -1286,7 +1290,9 @@ public class NotificationStackScrollLayout @Override public void setHeadsUpBottom(float headsUpBottom) { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; - if (mAmbientState.getHeadsUpBottom() != headsUpBottom) { + if (NotificationsHunSharedAnimationValues.isEnabled()) { + mHeadsUpAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom)); + } else if (mAmbientState.getHeadsUpBottom() != headsUpBottom) { mAmbientState.setHeadsUpBottom(headsUpBottom); mStateAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom)); } @@ -3074,20 +3080,20 @@ public class NotificationStackScrollLayout */ private boolean removeRemovedChildFromHeadsUpChangeAnimations(View child) { boolean hasAddEvent = false; - for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { - ExpandableNotificationRow row = eventPair.first; - boolean isHeadsUp = eventPair.second; + for (HeadsUpAnimationEvent event : mHeadsUpChangeAnimations.values()) { + ExpandableNotificationRow row = event.getRow(); + boolean isHeadsUp = event.isHeadsUpAppearance(); if (child == row) { - mTmpList.add(eventPair); + mTmpHeadsUpChangeAnimations.add(event.getRow()); hasAddEvent |= isHeadsUp; } } if (hasAddEvent) { // This child was just added lets remove all events. - mHeadsUpChangeAnimations.removeAll(mTmpList); + mTmpHeadsUpChangeAnimations.forEach((row) -> mHeadsUpChangeAnimations.remove(row)); ((ExpandableNotificationRow) child).setHeadsUpAnimatingAway(false); } - mTmpList.clear(); + mTmpHeadsUpChangeAnimations.clear(); return hasAddEvent && mAddedHeadsUpChildren.contains(child); } @@ -3373,9 +3379,9 @@ public class NotificationStackScrollLayout } private void generateHeadsUpAnimationEvents() { - for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) { - ExpandableNotificationRow row = eventPair.first; - boolean isHeadsUp = eventPair.second; + for (HeadsUpAnimationEvent headsUpEvent : mHeadsUpChangeAnimations.values()) { + ExpandableNotificationRow row = headsUpEvent.getRow(); + boolean isHeadsUp = headsUpEvent.isHeadsUpAppearance(); if (isHeadsUp != row.isHeadsUp()) { // For cases where we have a heads up showing and appearing again we shouldn't // do the animations at all. @@ -3433,6 +3439,10 @@ public class NotificationStackScrollLayout } AnimationEvent event = new AnimationEvent(row, type); event.headsUpFromBottom = onBottom; + + boolean hasStatusBarChip = + StatusBarNotifChips.isEnabled() && headsUpEvent.getHasStatusBarChip(); + event.headsUpHasStatusBarChip = hasStatusBarChip; // TODO(b/283084712) remove this and update the HUN filters at creation event.filter.animateHeight = false; mAnimationEvents.add(event); @@ -5068,10 +5078,11 @@ public class NotificationStackScrollLayout mAnimationFinishedRunnables.add(runnable); } - public void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) { + public void generateHeadsUpAnimation( + NotificationEntry entry, boolean isHeadsUp, boolean hasStatusBarChip) { SceneContainerFlag.assertInLegacyMode(); ExpandableNotificationRow row = entry.getHeadsUpAnimationView(); - generateHeadsUpAnimation(row, isHeadsUp); + generateHeadsUpAnimation(row, isHeadsUp, hasStatusBarChip); } /** @@ -5080,8 +5091,11 @@ public class NotificationStackScrollLayout * * @param row to animate * @param isHeadsUp true for appear, false for disappear animations + * @param hasStatusBarChip true if the status bar is currently displaying a chip for the given + * notification */ - public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) { + public void generateHeadsUpAnimation( + ExpandableNotificationRow row, boolean isHeadsUp, boolean hasStatusBarChip) { boolean addAnimation = mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed); if (NotificationThrottleHun.isEnabled()) { @@ -5096,19 +5110,26 @@ public class NotificationStackScrollLayout : " isSeenInShade=" + row.getEntry().isSeenInShade() + " row=" + row.getKey()) + " mIsExpanded=" + mIsExpanded - + " isHeadsUp=" + isHeadsUp); + + " isHeadsUp=" + isHeadsUp + + " hasStatusBarChip=" + hasStatusBarChip); } + if (addAnimation) { // If we're hiding a HUN we just started showing THIS FRAME, then remove that event, // and do not add the disappear event either. - if (!isHeadsUp && mHeadsUpChangeAnimations.remove(new Pair<>(row, true))) { + boolean showingHunThisFrame = + mHeadsUpChangeAnimations.containsKey(row) + && mHeadsUpChangeAnimations.get(row).isHeadsUpAppearance(); + if (!isHeadsUp && showingHunThisFrame) { + mHeadsUpChangeAnimations.remove(row); if (SPEW) { Log.v(TAG, "generateHeadsUpAnimation: previous hun appear animation cancelled"); } logHunAnimationSkipped(row, "previous hun appear animation cancelled"); return; } - mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp)); + mHeadsUpChangeAnimations.put( + row, new HeadsUpAnimationEvent(row, isHeadsUp, hasStatusBarChip)); mNeedsAnimation = true; if (!mIsExpanded && !mWillExpand && !isHeadsUp) { row.setHeadsUpAnimatingAway(true); @@ -5116,6 +5137,9 @@ public class NotificationStackScrollLayout setHeadsUpAnimatingAway(true); } } + if (StatusBarNotifChips.isEnabled()) { + row.setHasStatusBarChipDuringHeadsUpAnimation(hasStatusBarChip); + } requestChildrenUpdate(); } } @@ -6469,30 +6493,50 @@ public class NotificationStackScrollLayout static AnimationFilter[] FILTERS = new AnimationFilter[]{ // ANIMATION_TYPE_ADD - new AnimationFilter() - .animateAlpha() - .animateHeight() - .animateTopInset() - .animateY() - .animateZ() - .hasDelays(), + physicalNotificationMovement() + ? new AnimationFilter() + .animateAlpha() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ() + : new AnimationFilter() + .animateAlpha() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ() + .hasDelays(), // ANIMATION_TYPE_REMOVE - new AnimationFilter() - .animateAlpha() - .animateHeight() - .animateTopInset() - .animateY() - .animateZ() - .hasDelays(), + physicalNotificationMovement() + ? new AnimationFilter() + .animateAlpha() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ() + : new AnimationFilter() + .animateAlpha() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ() + .hasDelays(), // ANIMATION_TYPE_REMOVE_SWIPED_OUT - new AnimationFilter() - .animateHeight() - .animateTopInset() - .animateY() - .animateZ() - .hasDelays(), + physicalNotificationMovement() + ? new AnimationFilter() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ() + : new AnimationFilter() + .animateHeight() + .animateTopInset() + .animateY() + .animateZ() + .hasDelays(), // ANIMATION_TYPE_TOP_PADDING_CHANGED new AnimationFilter() @@ -6682,6 +6726,7 @@ public class NotificationStackScrollLayout final long length; View viewAfterChangingView; boolean headsUpFromBottom; + boolean headsUpHasStatusBarChip; AnimationEvent(ExpandableView view, int type) { this(view, type, LENGTHS[type]); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 124e6f590bfe..bb3abc1fba38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -92,6 +92,7 @@ import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.ColorUpdateLogger; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; @@ -325,6 +326,14 @@ public class NotificationStackScrollLayoutController implements Dumpable { */ private float mMaxAlphaForGlanceableHub = 1.0f; + /** + * A list of keys for the visible status bar chips. + * + * Note that this list can contain both notification keys, as well as keys for other types of + * chips like screen recording. + */ + private List<String> mVisibleStatusBarChipKeys = new ArrayList<>(); + private final NotificationListViewBinder mViewBinder; private void updateResources() { @@ -1580,8 +1589,16 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mView.getFirstChildNotGone(); } + /** Sets the list of keys that have currently visible status bar chips. */ + public void updateStatusBarChipKeys(List<String> visibleStatusBarChipKeys) { + mVisibleStatusBarChipKeys = visibleStatusBarChipKeys; + } + public void generateHeadsUpAnimation(NotificationEntry entry, boolean isHeadsUp) { - mView.generateHeadsUpAnimation(entry, isHeadsUp); + boolean hasStatusBarChip = + StatusBarNotifChips.isEnabled() + && mVisibleStatusBarChipKeys.contains(entry.getKey()); + mView.generateHeadsUpAnimation(entry, isHeadsUp, hasStatusBarChip); } public void setMaxTopPadding(int padding) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 4effb76c6570..d23a4c6307fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -1059,7 +1059,9 @@ public class StackScrollAlgorithm { shouldHunAppearFromBottom(ambientState, childState); if (NotificationsHunSharedAnimationValues.isEnabled()) { int yTranslation = - mHeadsUpAnimator.getHeadsUpYTranslation(shouldHunAppearFromBottom); + mHeadsUpAnimator.getHeadsUpYTranslation( + shouldHunAppearFromBottom, + row.hasStatusBarChipDuringHeadsUpAnimation()); childState.setYTranslation(yTranslation); } else { if (shouldHunAppearFromBottom) { 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 19abfa8140df..5414318b29bd 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 @@ -38,6 +38,7 @@ import com.android.internal.dynamicanimation.animation.DynamicAnimation; import com.android.systemui.res.R; import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.statusbar.NotificationShelf; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator; import com.android.systemui.statusbar.notification.headsup.HeadsUpAnimator; import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues; @@ -289,6 +290,10 @@ public class StackStateAnimator { long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING; switch (event.animationType) { case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: { + if (physicalNotificationMovement()) { + // We don't want any delays when adding anymore + continue; + } int ownIndex = viewState.notGoneIndex; int changingIndex = ((ExpandableView) (event.mChangingView)).getViewState().notGoneIndex; @@ -302,6 +307,10 @@ public class StackStateAnimator { case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT: delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL; case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: { + if (physicalNotificationMovement()) { + // We don't want any delays when removing anymore + continue; + } int ownIndex = viewState.notGoneIndex; boolean noNextView = event.viewAfterChangingView == null; ExpandableView viewAfterChangingView = noNextView @@ -552,7 +561,9 @@ public class StackStateAnimator { mHeadsUpAppearChildren.add(changingView); mTmpState.copyFrom(changingView.getViewState()); - mTmpState.setYTranslation(getHeadsUpYTranslationStart(event.headsUpFromBottom)); + mTmpState.setYTranslation( + getHeadsUpYTranslationStart( + event.headsUpFromBottom, event.headsUpHasStatusBarChip)); // set the height and the initial position mTmpState.applyToView(changingView); mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y, @@ -664,7 +675,9 @@ public class StackStateAnimator { // StackScrollAlgorithm cannot find this view because it has been removed // from the NSSL. To correctly translate the view to the top or bottom of // the screen (where it animated from), we need to update its translation. - mTmpState.setYTranslation(getHeadsUpYTranslationStart(event.headsUpFromBottom)); + mTmpState.setYTranslation( + getHeadsUpYTranslationStart( + event.headsUpFromBottom, event.headsUpHasStatusBarChip)); endRunnable = changingView::removeFromTransientContainer; } @@ -735,9 +748,9 @@ public class StackStateAnimator { return needsCustomAnimation; } - private float getHeadsUpYTranslationStart(boolean headsUpFromBottom) { + private float getHeadsUpYTranslationStart(boolean headsUpFromBottom, boolean hasStatusBarChip) { if (NotificationsHunSharedAnimationValues.isEnabled()) { - return mHeadsUpAnimator.getHeadsUpYTranslation(headsUpFromBottom); + return mHeadsUpAnimator.getHeadsUpYTranslation(headsUpFromBottom, hasStatusBarChip); } if (headsUpFromBottom) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 1c079c198cd4..facb8941f1fa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -33,6 +33,7 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.NotificationActivityStarter import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController import com.android.systemui.statusbar.notification.dagger.SilentHeader @@ -133,6 +134,14 @@ constructor( } } + if (StatusBarNotifChips.isEnabled) { + launch { + viewModel.visibleStatusBarChipKeys.collect { keys -> + viewController.updateStatusBarChipKeys(keys) + } + } + } + launch { bindLogger(view) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index 5ed1889de01e..c1eb70ed7d25 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -20,6 +20,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor @@ -56,6 +57,7 @@ class NotificationListViewModel constructor( val shelf: NotificationShelfViewModel, val hideListViewModel: HideListViewModel, + val ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, val footerViewModelFactory: FooterViewModel.Factory, val emptyShadeViewModelFactory: EmptyShadeViewModel.Factory, val logger: Optional<NotificationLoggerViewModel>, @@ -364,6 +366,14 @@ constructor( } } + /** + * A list of keys for the visible status bar chips. + * + * Note that this list can contain both notification keys, as well as keys for other types of + * chips like screen recording. + */ + val visibleStatusBarChipKeys = ongoingActivityChipsViewModel.visibleChipKeys + // TODO(b/325936094) use it for the text displayed in the StatusBar fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel = HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key)) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index a277597e23df..c1aa5f12aa99 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -266,7 +266,7 @@ constructor( combine(shadeModeInteractor.shadeMode, shadeInteractor.qsExpansion) { shadeMode, qsExpansion -> when (shadeMode) { - is ShadeMode.Dual -> false + is ShadeMode.Dual, is ShadeMode.Split -> true is ShadeMode.Single -> qsExpansion < 0.5f } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt index feb74098f071..bc533148f514 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.ui.viewbinder +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModel import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout @@ -27,20 +29,29 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import com.android.app.tracing.coroutines.launchTraced as launch class HeadsUpNotificationViewBinder @Inject -constructor(private val viewModel: NotificationListViewModel) { +constructor( + private val viewModel: NotificationListViewModel, + private val ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, +) { suspend fun bindHeadsUpNotifications(parentView: NotificationStackScrollLayout): Unit = coroutineScope { launch { var previousKeys = emptySet<HeadsUpRowKey>() - combine(viewModel.pinnedHeadsUpRowKeys, viewModel.activeHeadsUpRowKeys, ::Pair) + combine( + viewModel.pinnedHeadsUpRowKeys, + viewModel.activeHeadsUpRowKeys, + ongoingActivityChipsViewModel.visibleChipKeys, + ::Triple, + ) .sample(viewModel.headsUpAnimationsEnabled, ::Pair) .collect { (newKeys, animationsEnabled) -> val pinned = newKeys.first val all = newKeys.second + val statusBarChips: List<String> = newKeys.third + val added = all.union(pinned) - previousKeys val removed = previousKeys - pinned previousKeys = pinned @@ -48,15 +59,23 @@ constructor(private val viewModel: NotificationListViewModel) { if (animationsEnabled) { added.forEach { key -> + val row = obtainView(key) + val hasStatusBarChip = statusBarChips.contains(row.entry.key) parentView.generateHeadsUpAnimation( - obtainView(key), + row, /* isHeadsUp = */ true, + hasStatusBarChip, ) } removed.forEach { key -> val row = obtainView(key) + val hasStatusBarChip = statusBarChips.contains(row.entry.key) if (!parentView.isBeingDragged()) { - parentView.generateHeadsUpAnimation(row, /* isHeadsUp= */ false) + parentView.generateHeadsUpAnimation( + row, + /* isHeadsUp= */ false, + hasStatusBarChip, + ) } row.markHeadsUpSeen() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarChipsModernization.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarChipsModernization.kt index 6afcd8a4fad8..0ac87178086f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarChipsModernization.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarChipsModernization.kt @@ -52,7 +52,9 @@ object StatusBarChipsModernization { * the flag is not enabled to ensure that the refactor author catches issues in testing. */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt index d53cbabb1d19..342adc6af003 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt @@ -65,12 +65,12 @@ class BatteryInteractor @Inject constructor(repo: BatteryRepository) { */ val batteryAttributionType = combine(isCharging, powerSave, isBatteryDefenderEnabled) { charging, powerSave, defend -> - if (charging) { - BatteryAttributionModel.Charging - } else if (powerSave) { + if (powerSave) { BatteryAttributionModel.PowerSave } else if (defend) { BatteryAttributionModel.Defend + } else if (charging) { + BatteryAttributionModel.Charging } else { null } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ethernet/shared/StatusBarSignalPolicyRefactorEthernet.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ethernet/shared/StatusBarSignalPolicyRefactorEthernet.kt index 1d1cfd6cdf68..b2d314c3590b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ethernet/shared/StatusBarSignalPolicyRefactorEthernet.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ethernet/shared/StatusBarSignalPolicyRefactorEthernet.kt @@ -50,7 +50,9 @@ object StatusBarSignalPolicyRefactorEthernet { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index 2efc0579f333..d6105c2bd93f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -192,20 +192,16 @@ constructor( override val isDeviceEmergencyCallCapable: StateFlow<Boolean> = serviceStateChangedEvent .mapLatest { - val modems = telephonyManager.activeModemCount - - // Assume false for automotive devices which don't have the calling feature. - // TODO: b/398045526 to revisit the below. - val isAutomotive: Boolean = - context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) - val hasFeatureCalling: Boolean = + // TODO(b/400460777): check for hasSystemFeature only once + val hasRadioAccess: Boolean = context.packageManager.hasSystemFeature( - PackageManager.FEATURE_TELEPHONY_CALLING + PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS ) - if (isAutomotive && !hasFeatureCalling) { + if (!hasRadioAccess) { return@mapLatest false } + val modems = telephonyManager.activeModemCount // Check the service state for every modem. If any state reports emergency calling // capable, then consider the device to have emergency call capabilities (0..<modems) 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 d7348892356d..cfd50973924d 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 @@ -47,7 +47,8 @@ import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernizat import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel import javax.inject.Inject -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch /** @@ -153,39 +154,43 @@ constructor( OngoingActivityChipBinder.createBinding(primaryChipView) launch { - viewModel.primaryOngoingActivityChip.collect { primaryChipModel -> - OngoingActivityChipBinder.bind( - primaryChipModel, - primaryChipViewBinding, - iconViewStore, + combine( + viewModel.primaryOngoingActivityChip, + viewModel.canShowOngoingActivityChips, + ::Pair, ) + .distinctUntilChanged() + .collect { (primaryChipModel, areChipsAllowed) -> + OngoingActivityChipBinder.bind( + primaryChipModel, + primaryChipViewBinding, + iconViewStore, + ) - if (StatusBarRootModernization.isEnabled) { - launch { + if (StatusBarRootModernization.isEnabled) { bindLegacyPrimaryOngoingActivityChipWithVisibility( - viewModel, + areChipsAllowed, primaryChipModel, primaryChipViewBinding, ) - } - } else { - when (primaryChipModel) { - is OngoingActivityChipModel.Active -> - listener?.onOngoingActivityStatusChanged( - hasPrimaryOngoingActivity = true, - hasSecondaryOngoingActivity = false, - shouldAnimate = true, - ) - - is OngoingActivityChipModel.Inactive -> - listener?.onOngoingActivityStatusChanged( - hasPrimaryOngoingActivity = false, - hasSecondaryOngoingActivity = false, - shouldAnimate = primaryChipModel.shouldAnimate, - ) + } else { + when (primaryChipModel) { + is OngoingActivityChipModel.Active -> + listener?.onOngoingActivityStatusChanged( + hasPrimaryOngoingActivity = true, + hasSecondaryOngoingActivity = false, + shouldAnimate = true, + ) + + is OngoingActivityChipModel.Inactive -> + listener?.onOngoingActivityStatusChanged( + hasPrimaryOngoingActivity = false, + hasSecondaryOngoingActivity = false, + shouldAnimate = primaryChipModel.shouldAnimate, + ) + } } } - } } } @@ -199,49 +204,53 @@ constructor( view.requireViewById(R.id.ongoing_activity_chip_secondary) ) launch { - viewModel.ongoingActivityChipsLegacy.collectLatest { chips -> - OngoingActivityChipBinder.bind( - chips.primary, - primaryChipViewBinding, - iconViewStore, + combine( + viewModel.ongoingActivityChipsLegacy, + viewModel.canShowOngoingActivityChips, + ::Pair, ) - OngoingActivityChipBinder.bind( - chips.secondary, - secondaryChipViewBinding, - iconViewStore, - ) - - if (StatusBarRootModernization.isEnabled) { - launch { + .distinctUntilChanged() + .collect { (chips, areChipsAllowed) -> + OngoingActivityChipBinder.bind( + chips.primary, + primaryChipViewBinding, + iconViewStore, + ) + OngoingActivityChipBinder.bind( + chips.secondary, + secondaryChipViewBinding, + iconViewStore, + ) + if (StatusBarRootModernization.isEnabled) { bindOngoingActivityChipsWithVisibility( - viewModel, + areChipsAllowed, chips, primaryChipViewBinding, secondaryChipViewBinding, ) + } else { + listener?.onOngoingActivityStatusChanged( + hasPrimaryOngoingActivity = + chips.primary is OngoingActivityChipModel.Active, + hasSecondaryOngoingActivity = + chips.secondary is OngoingActivityChipModel.Active, + // TODO(b/364653005): Figure out the animation story here. + shouldAnimate = true, + ) } - } else { - listener?.onOngoingActivityStatusChanged( - hasPrimaryOngoingActivity = - chips.primary is OngoingActivityChipModel.Active, - hasSecondaryOngoingActivity = - chips.secondary is OngoingActivityChipModel.Active, - // TODO(b/364653005): Figure out the animation story here. - shouldAnimate = true, - ) - } - - viewModel.contentArea.collect { _ -> - OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions( - primaryChipViewBinding, - viewModel.ongoingActivityChipsLegacy.value.primary, - ) - OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions( - secondaryChipViewBinding, - viewModel.ongoingActivityChipsLegacy.value.secondary, - ) - view.requestLayout() } + } + launch { + viewModel.contentArea.collect { _ -> + OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions( + primaryChipViewBinding, + viewModel.ongoingActivityChipsLegacy.value.primary, + ) + OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding, + viewModel.ongoingActivityChipsLegacy.value.secondary, + ) + view.requestLayout() } } } @@ -249,7 +258,7 @@ constructor( if (SceneContainerFlag.isEnabled) { listener?.let { listener -> launch { - viewModel.isHomeStatusBarAllowedByScene.collect { + viewModel.isHomeStatusBarAllowed.collect { listener.onIsHomeStatusBarAllowedBySceneChanged(it) } } @@ -314,48 +323,42 @@ constructor( } /** Bind the (legacy) single primary ongoing activity chip with the status bar visibility */ - private suspend fun bindLegacyPrimaryOngoingActivityChipWithVisibility( - viewModel: HomeStatusBarViewModel, + private fun bindLegacyPrimaryOngoingActivityChipWithVisibility( + areChipsAllowed: Boolean, primaryChipModel: OngoingActivityChipModel, primaryChipViewBinding: OngoingActivityChipViewBinding, ) { - viewModel.canShowOngoingActivityChips.collectLatest { visible -> - if (!visible) { - primaryChipViewBinding.rootView.hide(shouldAnimateChange = false) - } else { - when (primaryChipModel) { - is OngoingActivityChipModel.Active -> { - primaryChipViewBinding.rootView.show(shouldAnimateChange = true) - } + if (!areChipsAllowed) { + primaryChipViewBinding.rootView.hide(shouldAnimateChange = false) + } else { + when (primaryChipModel) { + is OngoingActivityChipModel.Active -> { + primaryChipViewBinding.rootView.show(shouldAnimateChange = true) + } - is OngoingActivityChipModel.Inactive -> { - primaryChipViewBinding.rootView.hide( - state = View.GONE, - shouldAnimateChange = primaryChipModel.shouldAnimate, - ) - } + is OngoingActivityChipModel.Inactive -> { + primaryChipViewBinding.rootView.hide( + state = View.GONE, + shouldAnimateChange = primaryChipModel.shouldAnimate, + ) } } } } /** Bind the primary/secondary chips along with the home status bar's visibility */ - private suspend fun bindOngoingActivityChipsWithVisibility( - viewModel: HomeStatusBarViewModel, + private fun bindOngoingActivityChipsWithVisibility( + areChipsAllowed: Boolean, chips: MultipleOngoingActivityChipsModelLegacy, primaryChipViewBinding: OngoingActivityChipViewBinding, secondaryChipViewBinding: OngoingActivityChipViewBinding, ) { - viewModel.canShowOngoingActivityChips.collectLatest { canShow -> - if (!canShow) { - primaryChipViewBinding.rootView.hide(shouldAnimateChange = false) - secondaryChipViewBinding.rootView.hide(shouldAnimateChange = false) - } else { - primaryChipViewBinding.rootView.adjustVisibility(chips.primary.toVisibilityModel()) - secondaryChipViewBinding.rootView.adjustVisibility( - chips.secondary.toVisibilityModel() - ) - } + if (!areChipsAllowed) { + primaryChipViewBinding.rootView.hide(shouldAnimateChange = false) + secondaryChipViewBinding.rootView.hide(shouldAnimateChange = false) + } else { + primaryChipViewBinding.rootView.adjustVisibility(chips.primary.toVisibilityModel()) + secondaryChipViewBinding.rootView.adjustVisibility(chips.secondary.toVisibilityModel()) } } @@ -428,10 +431,15 @@ constructor( // See CollapsedStatusBarFragment#hide. private fun View.hide(state: Int = View.INVISIBLE, shouldAnimateChange: Boolean) { animate().cancel() - if (visibility == View.INVISIBLE || visibility == View.GONE) { + + if ( + (visibility == View.INVISIBLE && state == View.INVISIBLE) || + (visibility == View.GONE && state == View.GONE) + ) { return } - if (!shouldAnimateChange) { + val isAlreadyHidden = visibility == View.INVISIBLE || visibility == View.GONE + if (!shouldAnimateChange || isAlreadyHidden) { alpha = 0f visibility = state return @@ -495,7 +503,7 @@ interface StatusBarVisibilityChangeListener { /** * Called when the scene state has changed such that the home status bar is newly allowed or no - * longer allowed. See [HomeStatusBarViewModel.isHomeStatusBarAllowedByScene]. + * longer allowed. See [HomeStatusBarViewModel.isHomeStatusBarAllowed]. */ fun onIsHomeStatusBarAllowedBySceneChanged(isHomeStatusBarAllowedByScene: Boolean) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index 4189221d8a83..c91ea9a50028 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -196,13 +196,15 @@ fun StatusBarRoot( setContent { PlatformTheme { - val chips by + val chipsVisibilityModel by statusBarViewModel.ongoingActivityChips .collectAsStateWithLifecycle() - OngoingActivityChips( - chips = chips, - iconViewStore = iconViewStore, - ) + if (chipsVisibilityModel.areChipsAllowed) { + OngoingActivityChips( + chips = chipsVisibilityModel.chips, + iconViewStore = iconViewStore, + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/ChipsVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/ChipsVisibilityModel.kt new file mode 100644 index 000000000000..5cc432fc6771 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/model/ChipsVisibilityModel.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.systemui.statusbar.pipeline.shared.ui.model + +import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel + +data class ChipsVisibilityModel( + val chips: MultipleOngoingActivityChipsModel, + /** + * True if the chips are allowed to be shown and false otherwise (e.g. if we're on lockscreen). + */ + val areChipsAllowed: Boolean, +) 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 807e90567eb7..3bae91a0ebe3 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 @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.annotation.ColorInt import android.graphics.Rect +import android.view.Display import android.view.View import androidx.compose.runtime.getValue import com.android.app.tracing.coroutines.launchTraced as launch @@ -41,7 +42,9 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround 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 @@ -67,11 +70,13 @@ import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernizat import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor +import com.android.systemui.statusbar.pipeline.shared.ui.model.ChipsVisibilityModel import com.android.systemui.statusbar.pipeline.shared.ui.model.SystemInfoCombinedVisibilityModel import com.android.systemui.statusbar.pipeline.shared.ui.model.VisibilityModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import javax.inject.Provider import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.awaitCancellation @@ -125,7 +130,7 @@ interface HomeStatusBarViewModel : Activatable { val primaryOngoingActivityChip: StateFlow<OngoingActivityChipModel> /** All supported activity chips, whether they are currently active or not. */ - val ongoingActivityChips: StateFlow<MultipleOngoingActivityChipsModel> + val ongoingActivityChips: StateFlow<ChipsVisibilityModel> /** * The multiple ongoing activity chips that should be shown on the left-hand side of the status @@ -141,13 +146,12 @@ interface HomeStatusBarViewModel : Activatable { val popupChips: List<PopupChipModel.Shown> /** - * True if the current scene can show the home status bar (aka this status bar), and false if - * the current scene should never show the home status bar. + * True if the status bar should be visible. * * TODO(b/364360986): Once the is<SomeChildView>Visible flows are fully enabled, we shouldn't * need this flow anymore. */ - val isHomeStatusBarAllowedByScene: StateFlow<Boolean> + val isHomeStatusBarAllowed: StateFlow<Boolean> /** True if the home status bar is showing, and there is no HUN happening */ val canShowOngoingActivityChips: Flow<Boolean> @@ -221,6 +225,7 @@ constructor( statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore, @Background bgScope: CoroutineScope, @Background bgDispatcher: CoroutineDispatcher, + shadeDisplaysInteractor: Provider<ShadeDisplaysInteractor>, ) : HomeStatusBarViewModel, ExclusiveActivatable() { private val hydrator = Hydrator(traceName = "HomeStatusBarViewModel.hydrator") @@ -252,19 +257,49 @@ constructor( override val primaryOngoingActivityChip = ongoingActivityChipsViewModel.primaryChip - override val ongoingActivityChips = ongoingActivityChipsViewModel.chips - override val ongoingActivityChipsLegacy = ongoingActivityChipsViewModel.chipsLegacy override val popupChips get() = statusBarPopupChips.shownPopupChips - override val isHomeStatusBarAllowedByScene: StateFlow<Boolean> = + /** + * Whether the display of this statusbar has the shade window (that is hosting shade container + * and lockscreen, among other things). + */ + private val isShadeWindowOnThisDisplay = + if (ShadeWindowGoesAround.isEnabled) { + shadeDisplaysInteractor.get().displayId.map { shadeDisplayId -> + thisDisplayId == shadeDisplayId + } + } else { + // Shade doesn't move anywhere, it is always on the default display. + flowOf(thisDisplayId == Display.DEFAULT_DISPLAY) + } + + private val isShadeVisibleOnAnyDisplay = + if (SceneContainerFlag.isEnabled) { + sceneInteractor.currentOverlays.map { currentOverlays -> + (Overlays.NotificationsShade in currentOverlays || + Overlays.QuickSettingsShade in currentOverlays) + } + } else { + shadeInteractor.isAnyFullyExpanded + } + + private val isShadeVisibleOnThisDisplay: Flow<Boolean> = + combine(isShadeWindowOnThisDisplay, isShadeVisibleOnAnyDisplay) { + hasShade, + isShadeVisibleOnAnyDisplay -> + hasShade && isShadeVisibleOnAnyDisplay + } + + private val isHomeStatusBarAllowedByScene: Flow<Boolean> = combine( sceneInteractor.currentScene, - sceneInteractor.currentOverlays, + isShadeVisibleOnThisDisplay, sceneContainerOcclusionInteractor.invisibleDueToOcclusion, - ) { currentScene, currentOverlays, isOccluded -> + ) { currentScene, isShadeVisible, isOccluded -> + // All scenes have their own status bars, so we should only show the home status bar // if we're not in a scene. There are two exceptions: // 1) The shade (notifications or quick settings) is shown, because it has its own @@ -272,9 +307,7 @@ constructor( // 2) If the scene is occluded, then the occluding app needs to show the status bar. // (Fullscreen apps actually won't show the status bar but that's handled with the // rest of our fullscreen app logic, which lives elsewhere.) - (currentScene == Scenes.Gone && - Overlays.NotificationsShade !in currentOverlays && - Overlays.QuickSettingsShade !in currentOverlays) || isOccluded + (currentScene == Scenes.Gone && !isShadeVisible) || isOccluded } .distinctUntilChanged() .logDiffsForTable( @@ -282,7 +315,6 @@ constructor( columnName = COL_ALLOWED_BY_SCENE, initialValue = false, ) - .stateIn(bgScope, SharingStarted.WhileSubscribed(), initialValue = false) override val areNotificationsLightsOut: Flow<Boolean> = if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) { @@ -331,21 +363,29 @@ constructor( * if we shouldn't be showing any part of the home status bar. */ private val isHomeScreenStatusBarAllowedLegacy: Flow<Boolean> = - combine( - keyguardTransitionInteractor.currentKeyguardState, - shadeInteractor.isAnyFullyExpanded, - ) { currentKeyguardState, isShadeExpanded -> - (currentKeyguardState == GONE || currentKeyguardState == OCCLUDED) && !isShadeExpanded + combine(keyguardTransitionInteractor.currentKeyguardState, isShadeVisibleOnThisDisplay) { + currentKeyguardState, + isShadeVisibleOnThisDisplay -> + (currentKeyguardState == GONE || currentKeyguardState == OCCLUDED) && + !isShadeVisibleOnThisDisplay // TODO(b/364360986): Add edge cases, like secure camera launch. } - private val isHomeStatusBarAllowed: Flow<Boolean> = + // "Compat" to cover both legacy and Scene container case in one flow. + private val isHomeStatusBarAllowedCompat = if (SceneContainerFlag.isEnabled) { isHomeStatusBarAllowedByScene } else { isHomeScreenStatusBarAllowedLegacy } + override val isHomeStatusBarAllowed = + isHomeStatusBarAllowedCompat.stateIn( + bgScope, + SharingStarted.WhileSubscribed(), + initialValue = false, + ) + private val shouldHomeStatusBarBeVisible = combine( isHomeStatusBarAllowed, @@ -369,15 +409,6 @@ constructor( ) .flowOn(bgDispatcher) - private val isAnyChipVisible = - if (StatusBarChipsModernization.isEnabled) { - ongoingActivityChips.map { it.active.any { chip -> !chip.isHidden } } - } else if (StatusBarNotifChips.isEnabled) { - ongoingActivityChipsLegacy.map { it.primary is OngoingActivityChipModel.Active } - } else { - primaryOngoingActivityChip.map { it is OngoingActivityChipModel.Active } - } - /** * True if we need to hide the usual start side content in order to show the heads up * notification info. @@ -419,9 +450,38 @@ constructor( combine( isHomeStatusBarAllowed, keyguardInteractor.isSecureCameraActive, - headsUpNotificationInteractor.statusBarHeadsUpStatus, - ) { isHomeStatusBarAllowed, isSecureCameraActive, headsUpState -> - isHomeStatusBarAllowed && !isSecureCameraActive && !headsUpState.isPinned + hideStartSideContentForHeadsUp, + ) { isHomeStatusBarAllowed, isSecureCameraActive, hideStartSideContentForHeadsUp -> + isHomeStatusBarAllowed && !isSecureCameraActive && !hideStartSideContentForHeadsUp + } + + override val ongoingActivityChips = + combine(ongoingActivityChipsViewModel.chips, canShowOngoingActivityChips) { chips, canShow + -> + ChipsVisibilityModel(chips, areChipsAllowed = canShow) + } + .stateIn( + bgScope, + SharingStarted.WhileSubscribed(), + initialValue = + ChipsVisibilityModel( + chips = MultipleOngoingActivityChipsModel(), + areChipsAllowed = false, + ), + ) + + private val hasOngoingActivityChips = + if (StatusBarChipsModernization.isEnabled) { + ongoingActivityChips.map { it.chips.active.any { chip -> !chip.isHidden } } + } else if (StatusBarNotifChips.isEnabled) { + ongoingActivityChipsLegacy.map { it.primary is OngoingActivityChipModel.Active } + } else { + primaryOngoingActivityChip.map { it is OngoingActivityChipModel.Active } + } + + private val isAnyChipVisible = + combine(hasOngoingActivityChips, canShowOngoingActivityChips) { hasChips, canShowChips -> + hasChips && canShowChips } override val isClockVisible: Flow<VisibilityModel> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt index 16f24f1d5821..5d7ce91b332c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.Flags -import com.android.systemui.qs.panels.ui.compose.PagerDots +import com.android.systemui.common.ui.compose.PagerDots import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel @Composable diff --git a/packages/SystemUI/src/com/android/systemui/supervision/shared/DeprecateDpmSupervisionApis.kt b/packages/SystemUI/src/com/android/systemui/supervision/shared/DeprecateDpmSupervisionApis.kt index 59c05c2493c3..a94d51a21af3 100644 --- a/packages/SystemUI/src/com/android/systemui/supervision/shared/DeprecateDpmSupervisionApis.kt +++ b/packages/SystemUI/src/com/android/systemui/supervision/shared/DeprecateDpmSupervisionApis.kt @@ -50,7 +50,9 @@ object DeprecateDpmSupervisionApis { * Caution!! Using this check incorrectly will cause crashes in nextfood builds! */ @JvmStatic - inline fun unsafeAssertInNewMode() = RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) + @Deprecated("Avoid crashing.", ReplaceWith("if (this.isUnexpectedlyInLegacyMode()) return")) + inline fun unsafeAssertInNewMode() = + RefactorFlagUtils.unsafeAssertInNewMode(isEnabled, FLAG_NAME) /** * Called to ensure code is only run when the flag is disabled. This will throw an exception if diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt index bb8fe46be5b0..3ac6c7bc0c6b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt @@ -28,7 +28,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.volume.Events -import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent +import com.android.systemui.volume.dialog.dagger.factory.VolumeDialogComponentFactory import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor import javax.inject.Inject import kotlinx.coroutines.awaitCancellation @@ -37,7 +37,7 @@ class VolumeDialog @Inject constructor( @Application context: Context, - private val componentFactory: VolumeDialogComponent.Factory, + private val componentFactory: VolumeDialogComponentFactory, private val visibilityInteractor: VolumeDialogVisibilityInteractor, ) : ComponentDialog(context, R.style.Theme_SystemUI_Dialog_Volume) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt index 434f6b567048..36b2b48ece21 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.dialog.dagger +import com.android.systemui.volume.dialog.dagger.factory.VolumeDialogComponentFactory import com.android.systemui.volume.dialog.dagger.module.VolumeDialogModule import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope @@ -38,9 +39,9 @@ interface VolumeDialogComponent { fun sliderComponentFactory(): VolumeDialogSliderComponent.Factory @Subcomponent.Factory - interface Factory { + interface Factory : VolumeDialogComponentFactory { - fun create( + override fun create( /** * Provides a coroutine scope to use inside [VolumeDialogScope]. * [com.android.systemui.volume.dialog.VolumeDialogPlugin] manages the lifecycle of this diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/factory/VolumeDialogComponentFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/factory/VolumeDialogComponentFactory.kt new file mode 100644 index 000000000000..d909f26dd1dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/factory/VolumeDialogComponentFactory.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.volume.dialog.dagger.factory + +import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent +import kotlinx.coroutines.CoroutineScope + +/** Common interface for all dagger Subcomponent.Factory providing [VolumeDialogComponent]. */ +interface VolumeDialogComponentFactory { + + fun create(scope: CoroutineScope): VolumeDialogComponent +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt index 7b08317d5265..fcf4d110f269 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt @@ -16,10 +16,12 @@ package com.android.systemui.volume.dialog.dagger.module +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepository import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepositoryImpl import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder import com.android.systemui.volume.dialog.settings.ui.binder.VolumeDialogSettingsButtonViewBinder +import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSlidersViewBinder import com.android.systemui.volume.dialog.ui.binder.ViewBinder import dagger.Binds @@ -27,7 +29,7 @@ import dagger.Module import dagger.Provides /** Dagger module for volume dialog code in the volume package */ -@Module +@Module(subcomponents = [VolumeDialogSliderComponent::class]) interface VolumeDialogModule { @Binds @@ -38,6 +40,7 @@ interface VolumeDialogModule { companion object { @Provides + @VolumeDialog fun provideViewBinders( slidersViewBinder: VolumeDialogSlidersViewBinder, ringerViewBinder: VolumeDialogRingerViewBinder, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt index 547c51d1cefd..35752ae08260 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.dialog.dagger.module import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent +import com.android.systemui.volume.dialog.dagger.factory.VolumeDialogComponentFactory import com.android.systemui.volume.dialog.shared.model.CsdWarningConfigModel import com.android.systemui.volume.dialog.utils.VolumeTracer import com.android.systemui.volume.dialog.utils.VolumeTracerImpl @@ -27,6 +28,11 @@ import dagger.Provides @Module(subcomponents = [VolumeDialogComponent::class]) interface VolumeDialogPluginModule { + @Binds + fun bindVolumeDialogComponentFactory( + factory: VolumeDialogComponent.Factory + ): VolumeDialogComponentFactory + @Binds fun bindVolumeTracer(volumeTracer: VolumeTracerImpl): VolumeTracer companion object { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt index 577e47bb3b83..d0ed24d0b86d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.dialog.sliders.dagger import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel import dagger.BindsInstance import dagger.Subcomponent @@ -34,6 +35,8 @@ interface VolumeDialogSliderComponent { fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder + fun sliderViewModel(): VolumeDialogSliderViewModel + @Subcomponent.Factory interface Factory { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index 5de8fe54505f..11d9df4294c0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -37,6 +37,7 @@ import com.android.systemui.common.ui.view.onApplyWindowInsets import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R import com.android.systemui.util.kotlin.awaitCancellationThenDispose +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory @@ -72,7 +73,7 @@ constructor( private val viewModel: VolumeDialogViewModel, private val jankListenerFactory: JankListenerFactory, private val tracer: VolumeTracer, - private val viewBinders: List<@JvmSuppressWildcards ViewBinder>, + @VolumeDialog private val viewBinders: List<@JvmSuppressWildcards ViewBinder>, ) { private val halfOpenedOffsetPx: Float = diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt index 75a5768193cf..c81900da2581 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt @@ -69,9 +69,11 @@ class FlingOnBackAnimationCallbackTest : SysuiTestCase() { callback.onBackProgressed(backEventOf(0.6f, 32)) assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled) assertEquals("Assert interpolated progress", 0.6f, callback.progressEvent?.progress) - getInstrumentation().runOnMainSync { callback.onBackInvoked() } - // Assert that onBackInvoked is not called immediately... - assertFalse(callback.backInvokedCalled) + getInstrumentation().runOnMainSync { + callback.onBackInvoked() + // Assert that onBackInvoked is not called immediately. + assertFalse(callback.backInvokedCalled) + } // Instead the fling animation is played and eventually onBackInvoked is called. callback.backInvokedLatch.await(1000, TimeUnit.MILLISECONDS) assertTrue(callback.backInvokedCalled) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 5249620dbdd0..a1d038ad8554 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -120,9 +120,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { private lateinit var deviceEntryUdfpsTouchOverlayViewModel: DeviceEntryUdfpsTouchOverlayViewModel @Mock private lateinit var defaultUdfpsTouchOverlayViewModel: DefaultUdfpsTouchOverlayViewModel - @Mock - private lateinit var udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate - private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository + @Mock private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock private lateinit var shadeInteractor: ShadeInteractor @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams> @@ -185,7 +183,6 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, - udfpsKeyguardAccessibilityDelegate, keyguardTransitionInteractor, mSelectedUserInteractor, { deviceEntryUdfpsTouchOverlayViewModel }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java index 562481567536..5c893da45b8d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java @@ -28,7 +28,6 @@ import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBO import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_EXPANDED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED; -import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -60,7 +59,6 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastSender; -import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.screenshot.TimeoutHandler; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.util.concurrency.FakeExecutor; @@ -102,7 +100,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Mock private UiEventLogger mUiEventLogger; private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); - private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @Mock private Animator mAnimator; @@ -152,8 +149,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, new ClipData.Item("Test Item")); - - mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, true); // turned off for legacy tests } /** @@ -170,13 +165,13 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { getFakeBroadcastDispatcher(), mBroadcastSender, mTimeoutHandler, - mFeatureFlags, mClipboardUtils, mExecutor, mClipboardImageLoader, mClipboardTransitionExecutor, mClipboardIndicationProvider, - mUiEventLogger); + mUiEventLogger, + new ActionIntentCreator()); verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture()); mCallbacks = mOverlayCallbacksCaptor.getValue(); } @@ -193,7 +188,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { ClipData clipData = new ClipData("", new String[]{"image/png"}, new ClipData.Item(Uri.parse(""))); - mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); mOverlayController.setClipData(clipData, ""); @@ -208,7 +202,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { ClipData clipData = new ClipData("", new String[]{"resource/png"}, new ClipData.Item(Uri.parse(""))); - mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); mOverlayController.setClipData(clipData, ""); @@ -219,7 +212,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_setClipData_textData_legacy() { - mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); initController(); mOverlayController.setClipData(mSampleClipData, "abc"); @@ -232,7 +224,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_setClipData_sensitiveTextData_legacy() { - mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); initController(); ClipDescription description = mSampleClipData.getDescription(); @@ -250,7 +241,6 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase { @Test public void test_setClipData_repeatedCalls_legacy() { when(mAnimator.isRunning()).thenReturn(true); - mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); initController(); mOverlayController.setClipData(mSampleClipData, ""); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index 2001a3ea7ca0..dad08e014c0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -422,25 +422,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase( assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) } - @DisableSceneContainer - @Test - fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() { - verify(mediaDataManager).addListener(capture(listener)) - - testPlayerOrdering() - - // If smartspace is prioritized - listener.value.onSmartspaceMediaDataLoaded( - SMARTSPACE_KEY, - EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true), - true, - ) - - // Then it should be shown immediately after any actively playing controls - assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec) - assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec) - } - @Test fun testOrderWithSmartspace_notPrioritized() { testPlayerOrdering() @@ -571,146 +552,6 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase( verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!)) } - @DisableSceneContainer - @Test - fun testMediaLoaded_ScrollToActivePlayer() { - verify(mediaDataManager).addListener(capture(listener)) - - listener.value.onMediaDataLoaded( - PLAYING_LOCAL, - null, - DATA.copy( - active = true, - isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false, - ), - ) - listener.value.onMediaDataLoaded( - PAUSED_LOCAL, - null, - DATA.copy( - active = true, - isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false, - ), - ) - runAllReady() - // adding a media recommendation card. - listener.value.onSmartspaceMediaDataLoaded( - SMARTSPACE_KEY, - EMPTY_SMARTSPACE_MEDIA_DATA, - false, - ) - mediaCarouselController.shouldScrollToKey = true - // switching between media players. - listener.value.onMediaDataLoaded( - PLAYING_LOCAL, - PLAYING_LOCAL, - DATA.copy( - active = true, - isPlaying = false, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = true, - ), - ) - listener.value.onMediaDataLoaded( - PAUSED_LOCAL, - PAUSED_LOCAL, - DATA.copy( - active = true, - isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false, - ), - ) - runAllReady() - - assertEquals( - MediaPlayerData.getMediaPlayerIndex(PAUSED_LOCAL), - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex, - ) - } - - @DisableSceneContainer - @Test - fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() { - verify(mediaDataManager).addListener(capture(listener)) - - listener.value.onSmartspaceMediaDataLoaded( - SMARTSPACE_KEY, - EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true), - false, - ) - listener.value.onMediaDataLoaded( - PLAYING_LOCAL, - null, - DATA.copy( - active = true, - isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false, - ), - ) - runAllReady() - - var playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL) - assertEquals( - playerIndex, - mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex, - ) - assertEquals(playerIndex, 0) - - // Replaying the same media player one more time. - // And check that the card stays in its position. - mediaCarouselController.shouldScrollToKey = true - listener.value.onMediaDataLoaded( - PLAYING_LOCAL, - null, - DATA.copy( - active = true, - isPlaying = true, - playbackLocation = MediaData.PLAYBACK_LOCAL, - resumption = false, - packageName = "PACKAGE_NAME", - ), - ) - runAllReady() - playerIndex = MediaPlayerData.getMediaPlayerIndex(PLAYING_LOCAL) - assertEquals(playerIndex, 0) - } - - @DisableSceneContainer - @Test - fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() { - verify(mediaDataManager).addListener(capture(listener)) - - var result = false - mediaCarouselController.updateHostVisibility = { result = true } - - whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true) - listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) - - assertEquals(true, result) - } - - @DisableSceneContainer - @Test - fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() { - verify(mediaDataManager).addListener(capture(listener)) - - var result = false - mediaCarouselController.updateHostVisibility = { result = true } - - whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false) - listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false) - assertEquals(false, result) - - visualStabilityCallback.value.onReorderingAllowed() - assertEquals(true, result) - } - @Test fun testGetCurrentVisibleMediaContentIntent() { val clickIntent1 = mock(PendingIntent::class.java) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt index 9543032ef5ec..88fcc706f072 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt @@ -19,16 +19,12 @@ package com.android.systemui.media.controls.ui.controller import android.animation.Animator import android.animation.AnimatorSet import android.app.PendingIntent -import android.app.smartspace.SmartspaceAction -import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color -import android.graphics.Matrix import android.graphics.drawable.Animatable2 import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable @@ -47,7 +43,6 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.provider.Settings import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.testing.TestableLooper -import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.view.animation.Interpolator @@ -59,7 +54,6 @@ import android.widget.TextView import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.LiveData -import androidx.media.utils.MediaConstants import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId @@ -72,19 +66,15 @@ import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.media.controls.MediaTestUtils -import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.domain.pipeline.MediaDataManager -import com.android.systemui.media.controls.shared.model.KEY_SMARTSPACE_APP_NAME import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData import com.android.systemui.media.controls.shared.model.MediaNotificationAction -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.ui.binder.SeekBarObserver import com.android.systemui.media.controls.ui.view.GutsViewHolder import com.android.systemui.media.controls.ui.view.MediaViewHolder -import com.android.systemui.media.controls.ui.view.RecommendationViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.media.dialog.MediaOutputDialogManager @@ -216,32 +206,9 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var communalSceneInteractor: CommunalSceneInteractor - @Mock private lateinit var recommendationViewHolder: RecommendationViewHolder - @Mock private lateinit var smartspaceAction: SmartspaceAction - private lateinit var smartspaceData: SmartspaceMediaData - @Mock private lateinit var coverContainer1: ViewGroup - @Mock private lateinit var coverContainer2: ViewGroup - @Mock private lateinit var coverContainer3: ViewGroup - @Mock private lateinit var recAppIconItem: CachingIconView - @Mock private lateinit var recCardTitle: TextView - @Mock private lateinit var coverItem: ImageView - @Mock private lateinit var matrix: Matrix - private lateinit var recTitle1: TextView - private lateinit var recTitle2: TextView - private lateinit var recTitle3: TextView - private lateinit var recSubtitle1: TextView - private lateinit var recSubtitle2: TextView - private lateinit var recSubtitle3: TextView - @Mock private lateinit var recProgressBar1: SeekBar - @Mock private lateinit var recProgressBar2: SeekBar - @Mock private lateinit var recProgressBar3: SeekBar @Mock private lateinit var globalSettings: GlobalSettings - private val intent = - Intent().apply { - putExtras(Bundle().also { it.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) }) - setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + private val intent = Intent().apply { setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } private val pendingIntent = PendingIntent.getActivity( mContext, @@ -282,7 +249,6 @@ public class MediaControlPanelTest : SysuiTestCase() { mediaOutputDialogManager, mediaCarouselController, falsingManager, - clock, logger, keyguardStateController, activityIntentHelper, @@ -304,27 +270,6 @@ public class MediaControlPanelTest : SysuiTestCase() { initMediaViewHolderMocks() initDeviceMediaData(false, DEVICE_NAME) - - // Set up recommendation view - initRecommendationViewHolderMocks() - - // Set valid recommendation data - val extras = Bundle() - extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) - val intent = - Intent().apply { - putExtras(extras) - setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - whenever(smartspaceAction.intent).thenReturn(intent) - whenever(smartspaceAction.extras).thenReturn(extras) - smartspaceData = - EMPTY_SMARTSPACE_MEDIA_DATA.copy( - packageName = PACKAGE, - instanceId = instanceId, - recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction), - cardAction = smartspaceAction, - ) } private fun initGutsViewHolderMocks() { @@ -471,49 +416,6 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(viewHolder.loadingEffectView).thenReturn(loadingEffectView) } - /** Initialize elements for the recommendation view holder */ - private fun initRecommendationViewHolderMocks() { - recTitle1 = TextView(context) - recTitle2 = TextView(context) - recTitle3 = TextView(context) - recSubtitle1 = TextView(context) - recSubtitle2 = TextView(context) - recSubtitle3 = TextView(context) - - whenever(recommendationViewHolder.recommendations).thenReturn(view) - whenever(recommendationViewHolder.mediaAppIcons) - .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem)) - whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle) - whenever(recommendationViewHolder.mediaCoverItems) - .thenReturn(listOf(coverItem, coverItem, coverItem)) - whenever(recommendationViewHolder.mediaCoverContainers) - .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3)) - whenever(recommendationViewHolder.mediaTitles) - .thenReturn(listOf(recTitle1, recTitle2, recTitle3)) - whenever(recommendationViewHolder.mediaSubtitles) - .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3)) - whenever(recommendationViewHolder.mediaProgressBars) - .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3)) - whenever(coverItem.imageMatrix).thenReturn(matrix) - - // set ids for recommendation containers - whenever(coverContainer1.id).thenReturn(1) - whenever(coverContainer2.id).thenReturn(2) - whenever(coverContainer3.id).thenReturn(3) - - whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder) - - val actionIcon = Icon.createWithResource(context, R.drawable.ic_android) - whenever(smartspaceAction.icon).thenReturn(actionIcon) - - // Needed for card and item action click - val mockContext = mock(Context::class.java) - whenever(view.context).thenReturn(mockContext) - whenever(coverContainer1.context).thenReturn(mockContext) - whenever(coverContainer2.context).thenReturn(mockContext) - whenever(coverContainer3.context).thenReturn(mockContext) - } - @After fun tearDown() { session.release() @@ -1488,169 +1390,6 @@ public class MediaControlPanelTest : SysuiTestCase() { /* ***** END guts tests for the player ***** */ - /* ***** Guts tests for the recommendations ***** */ - - @Test - fun recommendations_longClick_isFalse() { - whenever(falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)).thenReturn(true) - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) - verify(viewHolder.player).onLongClickListener = captor.capture() - - captor.value.onLongClick(viewHolder.player) - verify(mediaViewController, never()).openGuts() - verify(mediaViewController, never()).closeGuts() - } - - @Test - fun recommendations_longClickWhenGutsClosed_gutsOpens() { - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - whenever(mediaViewController.isGutsVisible).thenReturn(false) - - val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) - verify(viewHolder.player).onLongClickListener = captor.capture() - - captor.value.onLongClick(viewHolder.player) - verify(mediaViewController).openGuts() - verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId)) - } - - @Test - fun recommendations_longClickWhenGutsOpen_gutsCloses() { - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - whenever(mediaViewController.isGutsVisible).thenReturn(true) - - val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) - verify(viewHolder.player).onLongClickListener = captor.capture() - - captor.value.onLongClick(viewHolder.player) - verify(mediaViewController, never()).openGuts() - verify(mediaViewController).closeGuts(false) - } - - @Test - fun recommendations_cancelButtonClick_animation() { - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - cancel.callOnClick() - - verify(mediaViewController).closeGuts(false) - } - - @Test - fun recommendations_settingsButtonClick() { - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - settings.callOnClick() - verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId)) - - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(activityStarter).startActivity(captor.capture(), eq(true)) - - assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS) - } - - @Test - fun recommendations_dismissButtonClick() { - val mediaKey = "key for dismissal" - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData.copy(targetId = mediaKey)) - - assertThat(dismiss.isEnabled).isEqualTo(true) - dismiss.callOnClick() - verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId)) - verify(mediaDataManager).dismissSmartspaceRecommendation(eq(mediaKey), anyLong()) - } - - @Test - fun recommendation_gutsOpen_contentDescriptionIsForGuts() { - whenever(mediaViewController.isGutsVisible).thenReturn(true) - player.attachRecommendation(recommendationViewHolder) - - val gutsTextString = "gutsText" - whenever(gutsText.text).thenReturn(gutsTextString) - player.bindRecommendation(smartspaceData) - - val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) - verify(viewHolder.player).contentDescription = descriptionCaptor.capture() - val description = descriptionCaptor.value.toString() - - assertThat(description).isEqualTo(gutsTextString) - } - - @Test - fun recommendation_gutsClosed_contentDescriptionIsForPlayer() { - whenever(mediaViewController.isGutsVisible).thenReturn(false) - player.attachRecommendation(recommendationViewHolder) - - player.bindRecommendation(smartspaceData) - - val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) - verify(viewHolder.player).contentDescription = descriptionCaptor.capture() - val description = descriptionCaptor.value.toString() - - assertThat(description) - .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header)) - } - - @Test - fun recommendation_gutsChangesFromOpenToClosed_contentDescriptionUpdated() { - // Start out open - whenever(mediaViewController.isGutsVisible).thenReturn(true) - whenever(gutsText.text).thenReturn("gutsText") - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - // Update to closed by long pressing - val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) - verify(viewHolder.player).onLongClickListener = captor.capture() - reset(viewHolder.player) - - whenever(mediaViewController.isGutsVisible).thenReturn(false) - captor.value.onLongClick(viewHolder.player) - - // Then content description is now the player content description - val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) - verify(viewHolder.player).contentDescription = descriptionCaptor.capture() - val description = descriptionCaptor.value.toString() - - assertThat(description) - .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header)) - } - - @Test - fun recommendation_gutsChangesFromClosedToOpen_contentDescriptionUpdated() { - // Start out closed - whenever(mediaViewController.isGutsVisible).thenReturn(false) - val gutsTextString = "gutsText" - whenever(gutsText.text).thenReturn(gutsTextString) - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - // Update to open by long pressing - val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) - verify(viewHolder.player).onLongClickListener = captor.capture() - reset(viewHolder.player) - - whenever(mediaViewController.isGutsVisible).thenReturn(true) - captor.value.onLongClick(viewHolder.player) - - // Then content description is now the guts content description - val descriptionCaptor = ArgumentCaptor.forClass(CharSequence::class.java) - verify(viewHolder.player).contentDescription = descriptionCaptor.capture() - val description = descriptionCaptor.value.toString() - - assertThat(description).isEqualTo(gutsTextString) - } - - /* ***** END guts tests for the recommendations ***** */ - @Test fun actionPlayPauseClick_isLogged() { val semanticActions = @@ -1887,578 +1626,6 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test - fun recommendation_gutsClosed_longPressOpens() { - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - whenever(mediaViewController.isGutsVisible).thenReturn(false) - - val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java) - verify(recommendationViewHolder.recommendations).setOnLongClickListener(captor.capture()) - - captor.value.onLongClick(recommendationViewHolder.recommendations) - verify(mediaViewController).openGuts() - verify(logger).logLongPressOpen(anyInt(), eq(PACKAGE), eq(instanceId)) - } - - @Test - fun recommendation_settingsButtonClick_isLogged() { - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - settings.callOnClick() - verify(logger).logLongPressSettings(anyInt(), eq(PACKAGE), eq(instanceId)) - - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(activityStarter).startActivity(captor.capture(), eq(true)) - - assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS) - } - - @Test - fun recommendation_dismissButton_isLogged() { - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - dismiss.callOnClick() - verify(logger).logLongPressDismiss(anyInt(), eq(PACKAGE), eq(instanceId)) - } - - @Test - fun recommendation_tapOnCard_isLogged() { - val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java) - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - verify(recommendationViewHolder.recommendations).setOnClickListener(captor.capture()) - captor.value.onClick(recommendationViewHolder.recommendations) - - verify(logger).logRecommendationCardTap(eq(PACKAGE), eq(instanceId)) - } - - @Test - fun recommendation_tapOnItem_isLogged() { - val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java) - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(smartspaceData) - - verify(coverContainer1).setOnClickListener(captor.capture()) - captor.value.onClick(recommendationViewHolder.recommendations) - - verify(logger).logRecommendationItemTap(eq(PACKAGE), eq(instanceId), eq(0)) - } - - @Test - fun bindRecommendation_listHasTooFewRecs_notDisplayed() { - player.attachRecommendation(recommendationViewHolder) - val icon = - Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle2") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - - player.bindRecommendation(data) - - assertThat(recTitle1.text).isEqualTo("") - verify(mediaViewController, never()).refreshState() - } - - @Test - fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() { - player.attachRecommendation(recommendationViewHolder) - val icon = - Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle2") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "empty icon 1") - .setSubtitle("subtitle2") - .setIcon(null) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "empty icon 2") - .setSubtitle("subtitle2") - .setIcon(null) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - - player.bindRecommendation(data) - - assertThat(recTitle1.text).isEqualTo("") - verify(mediaViewController, never()).refreshState() - } - - @Test - fun bindRecommendation_hasTitlesAndSubtitles() { - player.attachRecommendation(recommendationViewHolder) - - val title1 = "Title1" - val title2 = "Title2" - val title3 = "Title3" - val subtitle1 = "Subtitle1" - val subtitle2 = "Subtitle2" - val subtitle3 = "Subtitle3" - val icon = - Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) - - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", title1) - .setSubtitle(subtitle1) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", title2) - .setSubtitle(subtitle2) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", title3) - .setSubtitle(subtitle3) - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - player.bindRecommendation(data) - - assertThat(recTitle1.text).isEqualTo(title1) - assertThat(recTitle2.text).isEqualTo(title2) - assertThat(recTitle3.text).isEqualTo(title3) - assertThat(recSubtitle1.text).isEqualTo(subtitle1) - assertThat(recSubtitle2.text).isEqualTo(subtitle2) - assertThat(recSubtitle3.text).isEqualTo(subtitle3) - } - - @Test - fun bindRecommendation_noTitle_subtitleNotShown() { - player.attachRecommendation(recommendationViewHolder) - - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("fake subtitle") - .setIcon( - Icon.createWithResource( - context, - com.android.settingslib.R.drawable.ic_1x_mobiledata, - ) - ) - .setExtras(Bundle.EMPTY) - .build() - ) - ) - player.bindRecommendation(data) - - assertThat(recSubtitle1.text).isEqualTo("") - } - - @Test - fun bindRecommendation_someHaveTitles_allTitleViewsShown() { - useRealConstraintSets() - player.attachRecommendation(recommendationViewHolder) - - val icon = - Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "") - .setSubtitle("fake subtitle") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - player.bindRecommendation(data) - - assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.VISIBLE) - } - - @Test - fun bindRecommendation_someHaveSubtitles_allSubtitleViewsShown() { - useRealConstraintSets() - player.attachRecommendation(recommendationViewHolder) - - val icon = - Icon.createWithResource(context, com.android.settingslib.R.drawable.ic_1x_mobiledata) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle2") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("") - .setIcon(icon) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - player.bindRecommendation(data) - - assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.VISIBLE) - } - - @Test - fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() { - useRealConstraintSets() - player.attachRecommendation(recommendationViewHolder) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("") - .setIcon( - Icon.createWithResource( - context, - com.android.settingslib.R.drawable.ic_1x_mobiledata, - ) - ) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("") - .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("") - .setIcon( - Icon.createWithResource( - context, - com.android.settingslib.R.drawable.ic_3g_mobiledata, - ) - ) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - - player.bindRecommendation(data) - - assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE) - assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE) - assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE) - } - - @Test - fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() { - useRealConstraintSets() - player.attachRecommendation(recommendationViewHolder) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "") - .setSubtitle("subtitle1") - .setIcon( - Icon.createWithResource( - context, - com.android.settingslib.R.drawable.ic_1x_mobiledata, - ) - ) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "") - .setSubtitle("subtitle2") - .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm)) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "") - .setSubtitle("subtitle3") - .setIcon( - Icon.createWithResource( - context, - com.android.settingslib.R.drawable.ic_3g_mobiledata, - ) - ) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - - player.bindRecommendation(data) - - assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE) - assertThat(expandedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE) - assertThat(expandedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE) - assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE) - assertThat(expandedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE) - assertThat(expandedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(recTitle2.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(recTitle3.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(recSubtitle2.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(recSubtitle3.id)).isEqualTo(ConstraintSet.GONE) - } - - @Test - fun bindRecommendation_setAfterExecutors() { - val albumArt = getColorIcon(Color.RED) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(data) - bgExecutor.runAllReady() - mainExecutor.runAllReady() - - verify(recCardTitle).setTextColor(any<Int>()) - verify(recAppIconItem, times(3)).setImageDrawable(any<Drawable>()) - verify(coverItem, times(3)).setImageDrawable(any<Drawable>()) - verify(coverItem, times(3)).imageMatrix = any() - } - - @Test - fun bindRecommendationWithProgressBars() { - useRealConstraintSets() - val albumArt = getColorIcon(Color.RED) - val bundle = - Bundle().apply { - putInt( - MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED, - ) - putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, 0.5) - } - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(bundle) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(data) - - verify(recProgressBar1).setProgress(50) - verify(recProgressBar1).visibility = View.VISIBLE - verify(recProgressBar2).visibility = View.GONE - verify(recProgressBar3).visibility = View.GONE - assertThat(recSubtitle1.visibility).isEqualTo(View.GONE) - assertThat(recSubtitle2.visibility).isEqualTo(View.VISIBLE) - assertThat(recSubtitle3.visibility).isEqualTo(View.VISIBLE) - } - - @Test - fun bindRecommendation_carouselNotFitThreeRecs_OrientationPortrait() { - useRealConstraintSets() - val albumArt = getColorIcon(Color.RED) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - - // set the screen width less than the width of media controls. - player.context.resources.configuration.screenWidthDp = 350 - player.context.resources.configuration.orientation = Configuration.ORIENTATION_PORTRAIT - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(data) - - val res = player.context.resources - val displayAvailableWidth = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350f, res.displayMetrics).toInt() - val recCoverWidth: Int = - (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) + - res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2) - val numOfRecs = displayAvailableWidth / recCoverWidth - - assertThat(player.numberOfFittedRecommendations).isEqualTo(numOfRecs) - recommendationViewHolder.mediaCoverContainers.forEachIndexed { index, container -> - if (index < numOfRecs) { - assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(collapsedSet.getVisibility(container.id)) - .isEqualTo(ConstraintSet.VISIBLE) - } else { - assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) - } - } - } - - @Test - fun bindRecommendation_carouselNotFitThreeRecs_OrientationLandscape() { - useRealConstraintSets() - val albumArt = getColorIcon(Color.RED) - val data = - smartspaceData.copy( - recommendations = - listOf( - SmartspaceAction.Builder("id1", "title1") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id2", "title2") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - SmartspaceAction.Builder("id3", "title3") - .setSubtitle("subtitle1") - .setIcon(albumArt) - .setExtras(Bundle.EMPTY) - .build(), - ) - ) - - // set the screen width less than the width of media controls. - // We should have dp width less than 378 to test. In landscape we should have 2x. - player.context.resources.configuration.screenWidthDp = 700 - player.context.resources.configuration.orientation = Configuration.ORIENTATION_LANDSCAPE - player.attachRecommendation(recommendationViewHolder) - player.bindRecommendation(data) - - val res = player.context.resources - val displayAvailableWidth = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 350f, res.displayMetrics).toInt() - val recCoverWidth: Int = - (res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width) + - res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2) - val numOfRecs = displayAvailableWidth / recCoverWidth - - assertThat(player.numberOfFittedRecommendations).isEqualTo(numOfRecs) - recommendationViewHolder.mediaCoverContainers.forEachIndexed { index, container -> - if (index < numOfRecs) { - assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.VISIBLE) - assertThat(collapsedSet.getVisibility(container.id)) - .isEqualTo(ConstraintSet.VISIBLE) - } else { - assertThat(expandedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) - assertThat(collapsedSet.getVisibility(container.id)).isEqualTo(ConstraintSet.GONE) - } - } - } - - @Test - fun addTwoRecommendationGradients_differentStates() { - // Setup redArtwork and its color scheme. - val redArt = getColorIcon(Color.RED) - val redWallpaperColor = player.getWallpaperColor(redArt) - val redColorScheme = ColorScheme(redWallpaperColor, true, Style.CONTENT) - - // Setup greenArt and its color scheme. - val greenArt = getColorIcon(Color.GREEN) - val greenWallpaperColor = player.getWallpaperColor(greenArt) - val greenColorScheme = ColorScheme(greenWallpaperColor, true, Style.CONTENT) - - // Add gradient to both icons. - val redArtwork = player.addGradientToRecommendationAlbum(redArt, redColorScheme, 10, 10) - val greenArtwork = - player.addGradientToRecommendationAlbum(greenArt, greenColorScheme, 10, 10) - - // They should have different constant states as they have different gradient color. - assertThat(redArtwork.getDrawable(1).constantState) - .isNotEqualTo(greenArtwork.getDrawable(1).constantState) - } - - @Test fun onButtonClick_playsTouchRipple() { val semanticActions = MediaButton( diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt index e765b6f77155..760f73c726a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt @@ -42,7 +42,6 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.media.controls.ui.view.GutsViewHolder import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaViewHolder -import com.android.systemui.media.controls.ui.view.RecommendationViewHolder import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel import com.android.systemui.res.R import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView @@ -79,7 +78,6 @@ class MediaViewControllerTest : SysuiTestCase() { private val configurationController = com.android.systemui.statusbar.phone.ConfigurationControllerImpl(context) private var player = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) - private var recommendation = TransitionLayout(context, /* attrs */ null, /* defStyleAttr */ 0) private val clock = FakeSystemClock() private lateinit var mainExecutor: FakeExecutor private lateinit var seekBar: SeekBar @@ -110,9 +108,6 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var mockCopiedState: TransitionViewState @Mock private lateinit var detailWidgetState: WidgetState @Mock private lateinit var controlWidgetState: WidgetState - @Mock private lateinit var mediaTitleWidgetState: WidgetState - @Mock private lateinit var mediaSubTitleWidgetState: WidgetState - @Mock private lateinit var mediaContainerWidgetState: WidgetState @Mock private lateinit var seekBarViewModel: SeekBarViewModel @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress> @Mock private lateinit var globalSettings: GlobalSettings @@ -145,7 +140,7 @@ class MediaViewControllerTest : SysuiTestCase() { context: Context, animId: Int, motionInterpolator: Interpolator?, - vararg targets: View? + vararg targets: View?, ): AnimatorSet { return mockAnimator } @@ -158,7 +153,7 @@ class MediaViewControllerTest : SysuiTestCase() { fun testOrientationChanged_heightOfPlayerIsUpdated() { val newConfig = Configuration() - mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + mediaViewController.attach(player) // Change the height to see the effect of orientation change. MediaViewHolder.backgroundIds.forEach { id -> mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 10 @@ -177,30 +172,8 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test - fun testOrientationChanged_heightOfRecCardIsUpdated() { - val newConfig = Configuration() - - mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION) - // Change the height to see the effect of orientation change. - mediaViewController.expandedLayout - .getConstraint(RecommendationViewHolder.backgroundId) - .layout - .mHeight = 10 - newConfig.orientation = ORIENTATION_LANDSCAPE - configurationController.onConfigurationChanged(newConfig) - - assertTrue( - mediaViewController.expandedLayout - .getConstraint(RecommendationViewHolder.backgroundId) - .layout - .mHeight == - context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) - ) - } - - @Test fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { - mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + mediaViewController.attach(player) player.measureState = TransitionViewState().apply { this.height = 100 @@ -224,29 +197,8 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test - fun testObtainViewState_applySquishFraction_toRecommendationTransitionViewState_height() { - mediaViewController.attach(recommendation, MediaViewController.TYPE.RECOMMENDATION) - recommendation.measureState = TransitionViewState().apply { this.height = 100 } - mediaHostStateHolder.expansion = 1f - val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) - val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) - mediaHostStateHolder.measurementInput = - MeasurementInput(widthMeasureSpec, heightMeasureSpec) - - // Test no squish - mediaHostStateHolder.squishFraction = 1f - assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 100) - assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100) - - // Test half squish - mediaHostStateHolder.squishFraction = 0.5f - assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.height == 50) - assertTrue(mediaViewController.obtainViewState(mediaHostStateHolder)!!.measureHeight == 100) - } - - @Test fun testObtainViewState_expandedMatchesParentHeight() { - mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + mediaViewController.attach(player) player.measureState = TransitionViewState().apply { this.height = 100 @@ -283,7 +235,7 @@ class MediaViewControllerTest : SysuiTestCase() { .thenReturn( mutableMapOf( R.id.media_progress_bar to controlWidgetState, - R.id.header_artist to detailWidgetState + R.id.header_artist to detailWidgetState, ) ) whenever(mockCopiedState.measureHeight).thenReturn(200) @@ -311,7 +263,7 @@ class MediaViewControllerTest : SysuiTestCase() { .thenReturn( mutableMapOf( R.id.media_progress_bar to controlWidgetState, - R.id.header_artist to detailWidgetState + R.id.header_artist to detailWidgetState, ) ) whenever(mockCopiedState.measureHeight).thenReturn(200) @@ -332,46 +284,6 @@ class MediaViewControllerTest : SysuiTestCase() { verify(detailWidgetState, never()).alpha = floatThat { it > 0 } } - @Test - fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forRecommendation() { - whenever(mockViewState.copy()).thenReturn(mockCopiedState) - whenever(mockCopiedState.widgetStates) - .thenReturn( - mutableMapOf( - R.id.media_title to mediaTitleWidgetState, - R.id.media_subtitle to mediaSubTitleWidgetState, - R.id.media_cover1_container to mediaContainerWidgetState - ) - ) - whenever(mockCopiedState.measureHeight).thenReturn(360) - // media container widgets occupy [20, 300] - whenever(mediaContainerWidgetState.y).thenReturn(20F) - whenever(mediaContainerWidgetState.height).thenReturn(280) - whenever(mediaContainerWidgetState.alpha).thenReturn(1F) - // media title widgets occupy [320, 330] - whenever(mediaTitleWidgetState.y).thenReturn(320F) - whenever(mediaTitleWidgetState.height).thenReturn(10) - whenever(mediaTitleWidgetState.alpha).thenReturn(1F) - // media subtitle widgets occupy [340, 350] - whenever(mediaSubTitleWidgetState.y).thenReturn(340F) - whenever(mediaSubTitleWidgetState.height).thenReturn(10) - whenever(mediaSubTitleWidgetState.alpha).thenReturn(1F) - - // in current beizer, when the progress reach 0.38, the result will be 0.5 - mediaViewController.squishViewState(mockViewState, 307.6F / 360F) - verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - mediaViewController.squishViewState(mockViewState, 320F / 360F) - verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - // media title and media subtitle are in same widget group, should be calculate together and - // have same alpha - mediaViewController.squishViewState(mockViewState, 353.8F / 360F) - verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - mediaViewController.squishViewState(mockViewState, 360F / 360F) - verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - } - @EnableSceneContainer @Test fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() { 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 5c26dac5eb30..88c2697fe2f2 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 @@ -1573,25 +1573,6 @@ public class MediaSwitchingControllerTest extends SysuiTestCase { assertThat(items.get(1).isFirstDeviceInGroup()).isFalse(); } - @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) - @Test - public void deviceListUpdateWithDifferentDevices_firstSelectedDeviceIsFirstDeviceInGroup() { - when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); - doReturn(mMediaDevices) - .when(mLocalMediaManager) - .getSelectedMediaDevice(); - mMediaSwitchingController.start(mCb); - reset(mCb); - mMediaSwitchingController.getMediaItemList().clear(); - mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - mMediaDevices.clear(); - mMediaDevices.add(mMediaDevice2); - mMediaSwitchingController.onDeviceListUpdate(mMediaDevices); - - List<MediaItem> items = mMediaSwitchingController.getMediaItemList(); - assertThat(items.get(0).isFirstDeviceInGroup()).isTrue(); - } - private int getNumberOfConnectDeviceButtons() { int numberOfConnectDeviceButtons = 0; for (MediaItem item : mMediaSwitchingController.getMediaItemList()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt index 8c09b81744d7..e76be82c9340 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt @@ -25,6 +25,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -80,7 +82,9 @@ class EditModeTest : SysuiTestCase() { composeRule.assertCurrentTilesGridContainsExactly( listOf("tileA", "tileB", "tileC", "tileD_large", "tileE", "tileF") ) - composeRule.assertAvailableTilesGridContainsExactly(listOf("tileG_large")) + composeRule.assertAvailableTilesGridContainsExactly( + TestEditTiles.map { it.tile.tileSpec.spec } + ) } @Test @@ -88,7 +92,8 @@ class EditModeTest : SysuiTestCase() { composeRule.setContent { EditTileGridUnderTest() } composeRule.waitForIdle() - composeRule.onNodeWithContentDescription("tileA").performClick() // Selects + // Selects first "tileA", i.e. the one in the current grid + composeRule.onAllNodesWithText("tileA").onFirst().performClick() composeRule.onNodeWithText("Remove").performClick() // Removes composeRule.waitForIdle() @@ -96,7 +101,9 @@ class EditModeTest : SysuiTestCase() { composeRule.assertCurrentTilesGridContainsExactly( listOf("tileB", "tileC", "tileD_large", "tileE") ) - composeRule.assertAvailableTilesGridContainsExactly(listOf("tileA", "tileF", "tileG_large")) + composeRule.assertAvailableTilesGridContainsExactly( + TestEditTiles.map { it.tile.tileSpec.spec } + ) } private fun ComposeContentTestRule.assertCurrentTilesGridContainsExactly(specs: List<String>) = diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt index bd4c5f50eee7..021be3235ee1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt @@ -25,7 +25,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.click import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel import androidx.compose.ui.test.performTouchInput @@ -85,7 +86,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileA") + .onAllNodesWithText("tileA") + .onFirst() .performCustomAccessibilityActionWithLabel( context.getString(R.string.accessibility_qs_edit_toggle_tile_size_action) ) @@ -103,7 +105,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileD_large") + .onAllNodesWithText("tileD_large") + .onFirst() .performCustomAccessibilityActionWithLabel( context.getString(R.string.accessibility_qs_edit_toggle_tile_size_action) ) @@ -121,7 +124,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileA") + .onAllNodesWithText("tileA") + .onFirst() .performClick() // Select .performTouchInput { // Tap on resizing handle click(centerRight) @@ -141,7 +145,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileD_large") + .onAllNodesWithText("tileD_large") + .onFirst() .performClick() // Select .performTouchInput { // Tap on resizing handle click(centerRight) @@ -161,7 +166,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileA") + .onAllNodesWithText("tileA") + .onFirst() .performClick() // Select .performTouchInput { // Resize up swipeRight(startX = right, endX = right * 2) @@ -181,7 +187,8 @@ class ResizingTest : SysuiTestCase() { composeRule.waitForIdle() composeRule - .onNodeWithContentDescription("tileD_large") + .onAllNodesWithText("tileD_large") + .onFirst() .performClick() // Select .performTouchInput { // Resize down swipeLeft() diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt index 600572545d55..1f189a540aa2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionIntentCreatorTest.kt @@ -35,6 +35,10 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -43,9 +47,13 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) class ActionIntentCreatorTest : SysuiTestCase() { + private val scheduler = TestCoroutineScheduler() + private val mainDispatcher = UnconfinedTestDispatcher(scheduler) + private val testScope = TestScope(mainDispatcher) val context = mock<Context>() val packageManager = mock<PackageManager>() - private val actionIntentCreator = ActionIntentCreator(context, packageManager) + private val actionIntentCreator = + ActionIntentCreator(context, packageManager, testScope.backgroundScope, mainDispatcher) @Test fun testCreateShare() { @@ -132,7 +140,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEditLegacy() { + fun testCreateEditLegacy() = runTest { val uri = Uri.parse("content://fake") whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") @@ -155,7 +163,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEditLegacy_embeddedUserIdRemoved() { + fun testCreateEditLegacy_embeddedUserIdRemoved() = runTest { val uri = Uri.parse("content://555@fake") whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") @@ -166,7 +174,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEditLegacy_withEditor() { + fun testCreateEditLegacy_withEditor() = runTest { val uri = Uri.parse("content://fake") val component = ComponentName("com.android.foo", "com.android.foo.Something") @@ -180,7 +188,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit() { + fun testCreateEdit() = runTest { val uri = Uri.parse("content://fake") whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") @@ -203,7 +211,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit_embeddedUserIdRemoved() { + fun testCreateEdit_embeddedUserIdRemoved() = runTest { val uri = Uri.parse("content://555@fake") whenever(context.getString(eq(R.string.config_screenshotEditor))).thenReturn("") @@ -214,7 +222,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit_withPreferredEditorEnabled() { + fun testCreateEdit_withPreferredEditorEnabled() = runTest { val uri = Uri.parse("content://fake") val fallbackComponent = ComponentName("com.android.foo", "com.android.foo.Something") val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something") @@ -243,7 +251,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit_withPreferredEditorDisabled() { + fun testCreateEdit_withPreferredEditorDisabled() = runTest { val uri = Uri.parse("content://fake") val fallbackComponent = ComponentName("com.android.foo", "com.android.foo.Something") val preferredComponent = ComponentName("com.android.bar", "com.android.bar.Something") @@ -266,7 +274,7 @@ class ActionIntentCreatorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_USE_PREFERRED_IMAGE_EDITOR) - fun testCreateEdit_withFallbackEditor() { + fun testCreateEdit_withFallbackEditor() = runTest { val uri = Uri.parse("content://fake") val component = ComponentName("com.android.foo", "com.android.foo.Something") 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 acfa94a0218b..cd2ea7d25699 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 @@ -67,6 +67,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; import com.android.systemui.statusbar.notification.FeedbackIcon; import com.android.systemui.statusbar.notification.SourceType; @@ -1128,6 +1129,30 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { assertThat(row.mustStayOnScreen()).isFalse(); } + @Test + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + public void hasStatusBarChipDuringHeadsUpAnimation_flagOff_false() throws Exception { + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + + row.setHasStatusBarChipDuringHeadsUpAnimation(true); + + assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isFalse(); + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + public void hasStatusBarChipDuringHeadsUpAnimation_flagOn_returnsValue() throws Exception { + final ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + + assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isFalse(); + + row.setHasStatusBarChipDuringHeadsUpAnimation(true); + assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isTrue(); + + row.setHasStatusBarChipDuringHeadsUpAnimation(false); + assertThat(row.hasStatusBarChipDuringHeadsUpAnimation()).isFalse(); + } + private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable, Drawable rightIconDrawable) { ImageView iconView = mock(ImageView.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 2a58890f8767..8fb2a245921a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -82,6 +82,7 @@ import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; @@ -92,6 +93,7 @@ import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShade import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.headsup.AvalancheController; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; @@ -1260,7 +1262,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); // WHEN we generate a disappear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ false); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); // THEN headsUpAnimatingAway is true verify(headsUpAnimatingAwayListener).accept(true); @@ -1269,6 +1272,51 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test @EnableSceneContainer + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + public void testGenerateHeadsUpDisappearEvent_notifChipsFlagOff_statusBarChipNotSet() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ true); + + verify(row, never()).setHasStatusBarChipDuringHeadsUpAnimation(anyBoolean()); + } + + @Test + @EnableSceneContainer + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + public void testGenerateHeadsUpDisappearEvent_notifChipsFlagOn_statusBarChipSetToFalse() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); + + verify(row).setHasStatusBarChipDuringHeadsUpAnimation(false); + } + + @Test + @EnableSceneContainer + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + public void testGenerateHeadsUpDisappearEvent_notifChipsFlagOn_statusBarChipSetToTrue() { + // GIVEN NSSL is ready for HUN animations + Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); + + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ true); + + verify(row).setHasStatusBarChipDuringHeadsUpAnimation(true); + } + + @Test + @EnableSceneContainer public void testGenerateHeadsUpDisappearEvent_stackExpanded_headsUpAnimatingAwayNotSet() { // GIVEN NSSL would be ready for HUN animations, BUT it is expanded Consumer<Boolean> headsUpAnimatingAwayListener = mock(BooleanConsumer.class); @@ -1279,7 +1327,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mStackScroller.setHeadsUpGoingAwayAnimationsAllowed(true); // WHEN we generate a disappear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ false); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); // THEN nothing happens verify(headsUpAnimatingAwayListener, never()).accept(anyBoolean()); @@ -1294,10 +1343,12 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); // BUT there is a pending appear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ true, /* hasStatusBarChip= */ false); // WHEN we generate a disappear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ false); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); // THEN nothing happens verify(headsUpAnimatingAwayListener, never()).accept(anyBoolean()); @@ -1313,7 +1364,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); // WHEN we generate a disappear event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ true, /* hasStatusBarChip= */ false); // THEN headsUpAnimatingWay is not set verify(headsUpAnimatingAwayListener, never()).accept(anyBoolean()); @@ -1335,7 +1387,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { when(row.getEntry()).thenReturn(entry); // WHEN we generate an add event - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ true); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ true, /* hasStatusBarChip= */ false); // THEN nothing happens assertThat(mStackScroller.isAddOrRemoveAnimationPending()).isFalse(); @@ -1350,7 +1403,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { prepareStackScrollerForHunAnimations(headsUpAnimatingAwayListener); // AND there is a HUN animating away - mStackScroller.generateHeadsUpAnimation(row, /* isHeadsUp = */ false); + mStackScroller.generateHeadsUpAnimation( + row, /* isHeadsUp = */ false, /* hasStatusBarChip= */ false); assertTrue("a HUN should be animating away", mStackScroller.mHeadsUpAnimatingAway); // WHEN the child animations are finished diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorTest.kt index df45e2e21052..7946a68a6980 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase 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.statusbar.policy.batteryController import com.android.systemui.statusbar.policy.fake import com.android.systemui.testKosmos @@ -32,7 +33,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class BatteryInteractorTest : SysuiTestCase() { - val kosmos = testKosmos() + val kosmos = testKosmos().useUnconfinedTestDispatcher() val Kosmos.underTest by Kosmos.Fixture { batteryInteractor } @Test @@ -50,11 +51,61 @@ class BatteryInteractorTest : SysuiTestCase() { assertThat(latest).isEqualTo(BatteryAttributionModel.Defend) + batteryController.fake._isDefender = false + batteryController.fake._isPowerSave = true + + assertThat(latest).isEqualTo(BatteryAttributionModel.PowerSave) + + batteryController.fake._isPowerSave = false + batteryController.fake._isPluggedIn = true + + assertThat(latest).isEqualTo(BatteryAttributionModel.Charging) + } + + @Test + fun attributionType_prioritizesPowerSaveOverCharging() = + kosmos.runTest { + val latest by collectLastValue(underTest.batteryAttributionType) + + batteryController.fake._isPluggedIn = true + batteryController.fake._isDefender = true + batteryController.fake._isPowerSave = true + + assertThat(latest).isEqualTo(BatteryAttributionModel.PowerSave) + } + + @Test + fun attributionType_prioritizesPowerSaveOverDefender() = + kosmos.runTest { + val latest by collectLastValue(underTest.batteryAttributionType) + + batteryController.fake._isPluggedIn = true batteryController.fake._isPowerSave = true + batteryController.fake._isDefender = false assertThat(latest).isEqualTo(BatteryAttributionModel.PowerSave) + } + + @Test + fun attributionType_prioritizesDefenderOverCharging() = + kosmos.runTest { + val latest by collectLastValue(underTest.batteryAttributionType) batteryController.fake._isPluggedIn = true + batteryController.fake._isPowerSave = false + batteryController.fake._isDefender = true + + assertThat(latest).isEqualTo(BatteryAttributionModel.Defend) + } + + @Test + fun attributionType_prioritizesChargingOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.batteryAttributionType) + + batteryController.fake._isPluggedIn = true + batteryController.fake._isDefender = false + batteryController.fake._isPowerSave = false assertThat(latest).isEqualTo(BatteryAttributionModel.Charging) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelTest.kt index 6f4c745e8e7e..d8173486c8a8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelTest.kt @@ -97,4 +97,39 @@ class BatteryViewModelTest : SysuiTestCase() { assertThat(underTest.glyphList).isEqualTo(listOf(BatteryGlyph.BoltLarge)) } + + @Test + fun glyphList_attributionOrdering_prioritizesDefendOverCharging() = + kosmos.runTest { + fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 0) + batteryController.fake._level = 39 + batteryController.fake._isPluggedIn = true + batteryController.fake._isDefender = true + + assertThat(underTest.glyphList).isEqualTo(listOf(BatteryGlyph.DefendLarge)) + } + + @Test + fun glyphList_attributionOrdering_prioritizesPowerSaveOverDefend() = + kosmos.runTest { + fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 0) + batteryController.fake._level = 39 + batteryController.fake._isPluggedIn = true + batteryController.fake._isDefender = true + batteryController.fake._isPowerSave = true + + assertThat(underTest.glyphList).isEqualTo(listOf(BatteryGlyph.PlusLarge)) + } + + @Test + fun glyphList_attributionOrdering_prioritizesPowerSaveOverCharging() = + kosmos.runTest { + fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 0) + batteryController.fake._level = 39 + batteryController.fake._isPluggedIn = true + batteryController.fake._isDefender = false + batteryController.fake._isPowerSave = true + + assertThat(underTest.glyphList).isEqualTo(listOf(BatteryGlyph.PlusLarge)) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt index aa4dd18a6cba..74d53b088c9b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt @@ -20,6 +20,7 @@ import android.content.clipboardManager import android.content.res.mainResources import com.android.systemui.development.data.repository.developmentSettingRepository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.user.data.repository.userRepository @@ -31,5 +32,6 @@ val Kosmos.buildNumberInteractor by userRepository, { clipboardManager }, testDispatcher, + applicationCoroutineScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt index 697e7b9476ca..3f3c3c0d478a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.binder import android.content.applicationContext import android.view.mockedLayoutInflater import android.view.windowManager +import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor import com.android.systemui.biometrics.domain.interactor.fingerprintPropertyInteractor import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor import com.android.systemui.common.ui.domain.interactor.configurationInteractor @@ -37,6 +38,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.statusbar.gesture.TapGestureDetector +import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager import com.android.systemui.util.mockito.mock val Kosmos.alternateBouncerViewBinder by @@ -76,5 +78,7 @@ private val Kosmos.alternateBouncerUdfpsIconViewModel by fingerprintPropertyInteractor = fingerprintPropertyInteractor, udfpsOverlayInteractor = udfpsOverlayInteractor, alternateBouncerViewModel = alternateBouncerViewModel, + statusBarKeyguardViewManager = statusBarKeyguardViewManager, + accessibilityInteractor = accessibilityInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index af89403c5397..623989ec5809 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -67,6 +67,7 @@ import com.android.systemui.scene.shared.model.sceneDataSource import com.android.systemui.scene.ui.view.mockWindowRootViewProvider import com.android.systemui.settings.brightness.data.repository.brightnessMirrorShowingRepository import com.android.systemui.settings.displayTracker +import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeLayoutParams @@ -203,5 +204,6 @@ class KosmosJavaAdapter() { val windowRootViewBlurInteractor by lazy { kosmos.windowRootViewBlurInteractor } val sysuiState by lazy { kosmos.sysUiState } val displayTracker by lazy { kosmos.displayTracker } + val fakeShadeDisplaysRepository by lazy { kosmos.fakeShadeDisplaysRepository } val sysUIStateDispatcher by lazy { kosmos.sysUIStateDispatcher } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractorKosmos.kt deleted file mode 100644 index 1edd405f4af6..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractorKosmos.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.domain.pipeline.interactor - -import android.content.applicationContext -import com.android.systemui.broadcast.broadcastSender -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.media.controls.data.repository.mediaFilterRepository -import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor -import com.android.systemui.plugins.activityStarter - -val Kosmos.mediaRecommendationsInteractor by - Kosmos.Fixture { - MediaRecommendationsInteractor( - applicationScope = applicationCoroutineScope, - applicationContext = applicationContext, - repository = mediaFilterRepository, - mediaDataProcessor = mediaDataProcessor, - broadcastSender = broadcastSender, - activityStarter = activityStarter, - ) - } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt index 5e6434d84538..976b4046f58d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt @@ -37,7 +37,6 @@ val Kosmos.mediaCarouselViewModel by visualStabilityProvider = visualStabilityProvider, interactor = mediaCarouselInteractor, controlInteractorFactory = mediaControlInteractorFactory, - recommendationsViewModel = mediaRecommendationsViewModel, logger = mediaUiEventLogger, mediaLogger = mediaLogger, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelKosmos.kt deleted file mode 100644 index 34a527781979..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModelKosmos.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.controls.ui.viewmodel - -import android.content.applicationContext -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.media.controls.domain.pipeline.interactor.mediaRecommendationsInteractor -import com.android.systemui.media.controls.util.mediaUiEventLogger - -val Kosmos.mediaRecommendationsViewModel by - Kosmos.Fixture { - MediaRecommendationsViewModel( - applicationContext = applicationContext, - backgroundDispatcher = testDispatcher, - interactor = mediaRecommendationsInteractor, - logger = mediaUiEventLogger, - ) - } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt index 00deaafd7009..848fed6b0590 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/model/SysUiStateKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.model import android.view.Display import com.android.systemui.common.domain.interactor.SysUIStateDisplaysInteractor import com.android.systemui.display.data.repository.FakePerDisplayRepository +import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -45,5 +46,17 @@ val Kosmos.sysUiStateFactory by Fixture { val Kosmos.fakeSysUIStatePerDisplayRepository by Fixture { FakePerDisplayRepository<SysUiState>() } val Kosmos.sysuiStateInteractor by Fixture { - SysUIStateDisplaysInteractor(fakeSysUIStatePerDisplayRepository) + SysUIStateDisplaysInteractor(fakeSysUIStatePerDisplayRepository, displayRepository) +} + +val Kosmos.sysUiStateOverrideFactory by Fixture { + { displayId: Int -> + SysUIStateOverride( + displayId, + sceneContainerPlugin, + dumpManager, + sysUiState, + sysUIStateDispatcher, + ) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt index 43307701b6fb..4618dc78dcc6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt @@ -58,7 +58,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.settings.FakeGlobalSettings import com.android.systemui.util.settings.GlobalSettings import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler @@ -94,7 +94,7 @@ class FooterActionsTestUtils( return createFooterActionsViewModel( context, footerActionsInteractor, - flowOf(shadeMode), + MutableStateFlow(shadeMode), falsingManager, globalActionsDialogLite, mockActivityStarter, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt index 7145907a14a8..39391d03a44b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt @@ -18,14 +18,19 @@ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel import android.content.testableContext import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor -val Kosmos.mediaControlChipViewModel: MediaControlChipViewModel by +private val Kosmos.mediaControlChipViewModel: MediaControlChipViewModel by Kosmos.Fixture { MediaControlChipViewModel( - backgroundScope = applicationCoroutineScope, applicationContext = testableContext, mediaControlChipInteractor = mediaControlChipInteractor, ) } + +val Kosmos.mediaControlChipViewModelFactory by + Kosmos.Fixture { + object : MediaControlChipViewModel.Factory { + override fun create(): MediaControlChipViewModel = mediaControlChipViewModel + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt index b876095fefe5..2a3167cb66f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel import com.android.systemui.kosmos.Kosmos -import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.mediaControlChipViewModel +import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.mediaControlChipViewModelFactory private val Kosmos.statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel by - Kosmos.Fixture { StatusBarPopupChipsViewModel(mediaControlChip = mediaControlChipViewModel) } + Kosmos.Fixture { + StatusBarPopupChipsViewModel(mediaControlChipFactory = mediaControlChipViewModelFactory) + } val Kosmos.statusBarPopupChipsViewModelFactory by Kosmos.Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt index c337ac201b3d..8e98fe31a56a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt @@ -18,6 +18,10 @@ package com.android.systemui.statusbar.notification import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.phone.statusBarNotificationActivityStarter +import org.mockito.kotlin.mock var Kosmos.notificationActivityStarter: NotificationActivityStarter by Kosmos.Fixture { statusBarNotificationActivityStarter } + +var Kosmos.mockNotificationActivityStarter: NotificationActivityStarter by +Kosmos.Fixture { mock<NotificationActivityStarter>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerKosmos.kt new file mode 100644 index 000000000000..f3cdabb5813e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerKosmos.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.statusbar.notification.collection.render + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +var Kosmos.groupMembershipManager by Kosmos.Fixture { mock<GroupMembershipManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/people/NotificationPersonExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/people/NotificationPersonExtractorKosmos.kt new file mode 100644 index 000000000000..c263c5d96610 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/people/NotificationPersonExtractorKosmos.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.statusbar.notification.people + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.notificationPersonExtractor by Kosmos.Fixture { mock<NotificationPersonExtractor>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierKosmos.kt new file mode 100644 index 000000000000..20982eb43797 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierKosmos.kt @@ -0,0 +1,25 @@ +/* + * 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.notification.people + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.render.groupMembershipManager + +val Kosmos.peopleNotificationIdentifier by + Kosmos.Fixture { + PeopleNotificationIdentifierImpl(notificationPersonExtractor, groupMembershipManager) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt new file mode 100644 index 000000000000..e99f61e7cd13 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.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.statusbar.notification.row + +import com.android.internal.logging.metricsLogger +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactoryImpl +import com.android.systemui.statusbar.notification.collection.coordinator.visualStabilityCoordinator +import com.android.systemui.statusbar.notification.mockNotificationActivityStarter +import com.android.systemui.statusbar.notification.people.peopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider + +val Kosmos.entryAdapterFactory by +Kosmos.Fixture { + EntryAdapterFactoryImpl( + mockNotificationActivityStarter, + metricsLogger, + peopleNotificationIdentifier, + notificationIconStyleProvider, + visualStabilityCoordinator, + ) +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index 771e1a5dccc3..ff4fbf9f0e94 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -53,8 +53,11 @@ import com.android.systemui.statusbar.SmartReplyController import com.android.systemui.statusbar.notification.ColorUpdateLogger import com.android.systemui.statusbar.notification.ConversationNotificationManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor +import com.android.systemui.statusbar.notification.NotificationActivityStarter +import com.android.systemui.statusbar.notification.collection.EntryAdapterFactoryImpl import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider @@ -76,6 +79,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag import com.android.systemui.statusbar.notification.row.icon.AppIconProviderImpl +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProviderImpl import com.android.systemui.statusbar.notification.row.icon.NotificationRowIconViewInflaterFactory import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor @@ -364,11 +368,22 @@ class ExpandableNotificationRowBuilder( ) val row = rowInflaterTask.inflateSynchronously(context, null, entry) + val entryAdapter = + EntryAdapterFactoryImpl( + Mockito.mock(NotificationActivityStarter::class.java), + Mockito.mock(MetricsLogger::class.java), + Mockito.mock(PeopleNotificationIdentifier::class.java), + Mockito.mock(NotificationIconStyleProvider::class.java), + Mockito.mock(VisualStabilityCoordinator::class.java), + ) + .create(entry) + entry.row = row mIconManager.createIcons(entry) mBindPipelineEntryListener.onEntryInit(entry) mBindPipeline.manageRow(entry, row) row.initialize( + entryAdapter, entry, Mockito.mock(RemoteInputViewSubcomponent.Factory::class.java, STUB_ONLY), APP_NAME, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt index fbc2a21b0888..219ecbfe963b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.emptyShadeViewModelFactory @@ -35,6 +36,7 @@ val Kosmos.notificationListViewModel by Fixture { NotificationListViewModel( notificationShelfViewModel, hideListViewModel, + ongoingActivityChipsViewModel, footerViewModelFactory, emptyShadeViewModelFactory, Optional.of(notificationListLoggerViewModel), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt index 6a995c08ecae..2c5aed40b222 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt @@ -17,7 +17,13 @@ package com.android.systemui.statusbar.notification.ui.viewbinder import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel val Kosmos.headsUpNotificationViewBinder by - Kosmos.Fixture { HeadsUpNotificationViewBinder(viewModel = notificationListViewModel) } + Kosmos.Fixture { + HeadsUpNotificationViewBinder( + viewModel = notificationListViewModel, + ongoingActivityChipsViewModel = ongoingActivityChipsViewModel, + ) + } 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 a97c651ba426..5c4deaadffd5 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.kosmos.testDispatcher 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.shadeDisplaysInteractor 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 @@ -40,29 +41,34 @@ import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStat import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStatusBarInteractor var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by + Kosmos.Fixture { homeStatusBarViewModelFactory.invoke(testableContext.displayId) } +var Kosmos.homeStatusBarViewModelFactory: (Int) -> HomeStatusBarViewModel by Kosmos.Fixture { - HomeStatusBarViewModelImpl( - testableContext.displayId, - batteryViewModelFactory, - tableLogBufferFactory, - homeStatusBarInteractor, - homeStatusBarIconBlockListInteractor, - lightsOutInteractor, - activeNotificationsInteractor, - darkIconInteractor, - headsUpNotificationInteractor, - keyguardTransitionInteractor, - keyguardInteractor, - statusBarOperatorNameViewModel, - sceneInteractor, - sceneContainerOcclusionInteractor, - shadeInteractor, - shareToAppChipViewModel, - ongoingActivityChipsViewModel, - statusBarPopupChipsViewModelFactory, - systemStatusEventAnimationInteractor, - multiDisplayStatusBarContentInsetsViewModelStore, - backgroundScope, - testDispatcher, - ) + { displayId -> + HomeStatusBarViewModelImpl( + displayId, + batteryViewModelFactory, + tableLogBufferFactory, + homeStatusBarInteractor, + homeStatusBarIconBlockListInteractor, + lightsOutInteractor, + activeNotificationsInteractor, + darkIconInteractor, + headsUpNotificationInteractor, + keyguardTransitionInteractor, + keyguardInteractor, + statusBarOperatorNameViewModel, + sceneInteractor, + sceneContainerOcclusionInteractor, + shadeInteractor, + shareToAppChipViewModel, + ongoingActivityChipsViewModel, + statusBarPopupChipsViewModelFactory, + systemStatusEventAnimationInteractor, + multiDisplayStatusBarContentInsetsViewModelStore, + backgroundScope, + testDispatcher, + { shadeDisplaysInteractor }, + ) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt index 4ca044d60f3f..34218646b818 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt @@ -33,6 +33,8 @@ import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSlide import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder import com.android.systemui.volume.mediaControllerRepository @@ -60,6 +62,9 @@ fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDial } } + override fun sliderViewModel(): VolumeDialogSliderViewModel = + localKosmos.volumeDialogSliderViewModel + override fun sliderViewBinder(): VolumeDialogSliderViewBinder = localKosmos.volumeDialogSliderViewBinder diff --git a/packages/Vcn/service-b/Android.bp b/packages/Vcn/service-b/Android.bp index 97574e6e35e3..aad534c3289c 100644 --- a/packages/Vcn/service-b/Android.bp +++ b/packages/Vcn/service-b/Android.bp @@ -29,7 +29,10 @@ filegroup { "vcn-location-flag/platform/com/android/server/vcn/VcnLocation.java", ], }), - visibility: ["//frameworks/base/services/core"], + visibility: [ + "//frameworks/base/services/core", + "//packages/modules/Connectivity/service-t", + ], } // TODO: b/374174952 This library is only used in "service-connectivity-b-platform" diff --git a/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java b/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java index b9dcc6160d68..aac217b3cc7a 100644 --- a/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java +++ b/packages/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java @@ -37,9 +37,27 @@ public final class ConnectivityServiceInitializerB extends SystemService { private static final String TAG = ConnectivityServiceInitializerB.class.getSimpleName(); private final VcnManagementService mVcnManagementService; + // STOPSHIP: b/385203616 This static flag is for handling a temporary case when the mainline + // module prebuilt has updated to register the VCN but the platform change to remove + // registration is not merged. After mainline prebuilt is updated, we should merge the platform + // ASAP and remove this static check. This check is safe because both mainline and platform + // registration are triggered from the same method on the same thread. + private static boolean sIsRegistered = false; + public ConnectivityServiceInitializerB(Context context) { super(context); - mVcnManagementService = VcnManagementService.create(context); + + if (!sIsRegistered) { + mVcnManagementService = VcnManagementService.create(context); + sIsRegistered = true; + } else { + mVcnManagementService = null; + Log.e( + TAG, + "Ignore this registration since VCN is already registered. It will happen when" + + " the mainline module prebuilt has updated to register the VCN but the" + + " platform change to remove registration is not merged."); + } } @Override diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 42834ce20783..c49151dd5e30 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -287,7 +287,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public static final int INVALID_SERVICE_ID = -1; - // Each service has an ID. Also provide one for magnification gesture handling + // Each service has an ID. Also provide one for magnification gesture handling. + // This ID is also used for mouse event handling. public static final int MAGNIFICATION_GESTURE_HANDLER_ID = 0; private static int sIdCounter = MAGNIFICATION_GESTURE_HANDLER_ID + 1; diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index 57bbb4a7a0a7..90ddc43ae3ed 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -86,13 +86,6 @@ public class AutoclickTypePanel { }) public @interface Corner {} - private static final @Corner int[] CORNER_ROTATION_ORDER = { - CORNER_BOTTOM_RIGHT, - CORNER_BOTTOM_LEFT, - CORNER_TOP_LEFT, - CORNER_TOP_RIGHT - }; - // An interface exposed to {@link AutoclickController) to handle different actions on the panel, // including changing autoclick type, pausing/resuming autoclick. public interface ClickPanelControllerInterface { @@ -136,10 +129,9 @@ public class AutoclickTypePanel { // Whether autoclick is paused. private boolean mPaused = false; - // Tracks the current corner position of the panel using an index into CORNER_ROTATION_ORDER - // array. This allows the panel to cycle through screen corners in a defined sequence when - // repositioned. - private int mCurrentCornerIndex = 0; + + // The current corner position of the panel, default to bottom right. + private @Corner int mCurrentCorner = CORNER_BOTTOM_RIGHT; private final LinearLayout mLeftClickButton; private final LinearLayout mRightClickButton; @@ -257,13 +249,13 @@ public class AutoclickTypePanel { params.gravity = Gravity.START | Gravity.TOP; // Set the current corner to be bottom-left to ensure that the subsequent reposition // action rotates the panel clockwise from bottom-left towards top-left. - mCurrentCornerIndex = 1; + mCurrentCorner = CORNER_BOTTOM_LEFT; } else { // Snap to right edge. Set params.gravity to make sure x, y offsets from correct anchor. params.gravity = Gravity.END | Gravity.TOP; // Set the current corner to be top-right to ensure that the subsequent reposition // action rotates the panel clockwise from top-right towards bottom-right. - mCurrentCornerIndex = 3; + mCurrentCorner = CORNER_TOP_RIGHT; } // Apply final position: set params.x to be edge margin, params.y to maintain vertical @@ -415,10 +407,10 @@ public class AutoclickTypePanel { /** Moves the panel to the next corner in clockwise direction. */ private void moveToNextCorner() { - @Corner int nextCornerIndex = (mCurrentCornerIndex + 1) % CORNER_ROTATION_ORDER.length; - mCurrentCornerIndex = nextCornerIndex; + @Corner int nextCorner = (mCurrentCorner + 1) % 4; + mCurrentCorner = nextCorner; - setPanelPositionForCorner(mParams, mCurrentCornerIndex); + setPanelPositionForCorner(mParams, mCurrentCorner); mWindowManager.updateViewLayout(mContentView, mParams); } @@ -457,7 +449,7 @@ public class AutoclickTypePanel { String.valueOf(mParams.gravity), String.valueOf(mParams.x), String.valueOf(mParams.y), - String.valueOf(mCurrentCornerIndex) + String.valueOf(mCurrentCorner) }); Settings.Secure.putStringForUser(mContext.getContentResolver(), ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, positionString, mUserId); @@ -473,7 +465,7 @@ public class AutoclickTypePanel { ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, mUserId); if (savedPosition == null) { setPanelPositionForCorner(mParams, CORNER_BOTTOM_RIGHT); - mCurrentCornerIndex = 0; + mCurrentCorner = CORNER_BOTTOM_RIGHT; return; } @@ -481,7 +473,7 @@ public class AutoclickTypePanel { String[] parts = TextUtils.split(savedPosition, POSITION_DELIMITER); if (!isValidPositionParts(parts)) { setPanelPositionForCorner(mParams, CORNER_BOTTOM_RIGHT); - mCurrentCornerIndex = 0; + mCurrentCorner = CORNER_BOTTOM_RIGHT; return; } @@ -489,7 +481,7 @@ public class AutoclickTypePanel { mParams.gravity = Integer.parseInt(parts[0]); mParams.x = Integer.parseInt(parts[1]); mParams.y = Integer.parseInt(parts[2]); - mCurrentCornerIndex = Integer.parseInt(parts[3]); + mCurrentCorner = Integer.parseInt(parts[3]); } private boolean isValidPositionParts(String[] parts) { @@ -538,8 +530,8 @@ public class AutoclickTypePanel { @VisibleForTesting @Corner - int getCurrentCornerIndexForTesting() { - return mCurrentCornerIndex; + int getCurrentCornerForTesting() { + return mCurrentCorner; } @VisibleForTesting diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java index 11b8ccb70dfb..004b3ffcb02b 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java @@ -121,6 +121,8 @@ public class FullScreenMagnificationController implements @NonNull private final Supplier<MagnificationThumbnail> mThumbnailSupplier; @NonNull private final Supplier<Boolean> mMagnificationConnectionStateSupplier; + private boolean mIsPointerMotionFilterInstalled = false; + /** * This class implements {@link WindowManagerInternal.MagnificationCallbacks} and holds * magnification information per display. @@ -830,9 +832,17 @@ public class FullScreenMagnificationController implements return; } - final float nonNormOffsetX = mCurrentMagnificationSpec.offsetX - offsetX; - final float nonNormOffsetY = mCurrentMagnificationSpec.offsetY - offsetY; - if (updateCurrentSpecWithOffsetsLocked(nonNormOffsetX, nonNormOffsetY)) { + setOffset(mCurrentMagnificationSpec.offsetX - offsetX, + mCurrentMagnificationSpec.offsetY - offsetY, id); + } + + @GuardedBy("mLock") + void setOffset(float offsetX, float offsetY, int id) { + if (!mRegistered) { + return; + } + + if (updateCurrentSpecWithOffsetsLocked(offsetX, offsetY)) { onMagnificationChangedLocked(/* isScaleTransient= */ false); } if (id != INVALID_SERVICE_ID) { @@ -1065,6 +1075,7 @@ public class FullScreenMagnificationController implements if (display.register()) { mDisplays.put(displayId, display); mScreenStateObserver.registerIfNecessary(); + configurePointerMotionFilter(true); } } } @@ -1613,6 +1624,28 @@ public class FullScreenMagnificationController implements } /** + * Sets the offset of the magnified region. + * + * @param displayId The logical display id. + * @param offsetX the offset of the magnified region in the X coordinate, in current + * screen pixels. + * @param offsetY the offset of the magnified region in the Y coordinate, in current + * screen pixels. + * @param id the ID of the service requesting the change + */ + @SuppressWarnings("GuardedBy") + // errorprone cannot recognize an inner class guarded by an outer class member. + public void setOffset(int displayId, float offsetX, float offsetY, int id) { + synchronized (mLock) { + final DisplayMagnification display = mDisplays.get(displayId); + if (display == null) { + return; + } + display.setOffset(offsetX, offsetY, id); + } + } + + /** * Offsets the magnified region. Note that the offsetX and offsetY values actually move in the * opposite direction as the offsets passed in here. * @@ -1885,6 +1918,7 @@ public class FullScreenMagnificationController implements } if (!hasRegister) { mScreenStateObserver.unregister(); + configurePointerMotionFilter(false); } } @@ -1900,6 +1934,22 @@ public class FullScreenMagnificationController implements } } + private void configurePointerMotionFilter(boolean enabled) { + if (!Flags.enableMagnificationFollowsMouseWithPointerMotionFilter()) { + return; + } + if (enabled == mIsPointerMotionFilterInstalled) { + return; + } + if (!enabled) { + mControllerCtx.getInputManager().registerAccessibilityPointerMotionFilter(null); + } else { + mControllerCtx.getInputManager().registerAccessibilityPointerMotionFilter( + new FullScreenMagnificationPointerMotionEventFilter(this)); + } + mIsPointerMotionFilterInstalled = enabled; + } + private boolean traceEnabled() { return mControllerCtx.getTraceManager().isA11yTracingEnabledForTypes( FLAGS_WINDOW_MANAGER_INTERNAL); diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java index e0dd8b601a3d..59b4a1613e08 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java @@ -182,6 +182,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH private final int mMinimumVelocity; private final int mMaximumVelocity; + @Nullable private final MouseEventHandler mMouseEventHandler; public FullScreenMagnificationGestureHandler( @@ -313,7 +314,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH mOverscrollEdgeSlop = context.getResources().getDimensionPixelSize( R.dimen.accessibility_fullscreen_magnification_gesture_edge_slop); mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); - mMouseEventHandler = new MouseEventHandler(mFullScreenMagnificationController); + mMouseEventHandler = + Flags.enableMagnificationFollowsMouseWithPointerMotionFilter() + ? null : new MouseEventHandler(mFullScreenMagnificationController); if (mDetectShortcutTrigger) { mScreenStateReceiver = new ScreenStateReceiver(context, this); @@ -337,9 +340,11 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH @Override void handleMouseOrStylusEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - if (!mFullScreenMagnificationController.isActivated(mDisplayId)) { + if (mMouseEventHandler == null + || !mFullScreenMagnificationController.isActivated(mDisplayId)) { return; } + // TODO(b/354696546): Allow mouse/stylus to activate whichever display they are // over, rather than only interacting with the current display. diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationPointerMotionEventFilter.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationPointerMotionEventFilter.java new file mode 100644 index 000000000000..f1ba83e80d54 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationPointerMotionEventFilter.java @@ -0,0 +1,66 @@ +/* + * 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.accessibility.magnification; + +import static com.android.server.accessibility.AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID; + +import android.annotation.NonNull; + +import com.android.server.input.InputManagerInternal; + +/** + * Handles pointer motion event for full screen magnification. + * Responsible for controlling magnification's cursor following feature. + */ +public class FullScreenMagnificationPointerMotionEventFilter implements + InputManagerInternal.AccessibilityPointerMotionFilter { + + private final FullScreenMagnificationController mController; + + public FullScreenMagnificationPointerMotionEventFilter( + FullScreenMagnificationController controller) { + mController = controller; + } + + /** + * This call happens on the input hot path and it is extremely performance sensitive. It + * also must not call back into native code. + */ + @Override + @NonNull + public float[] filterPointerMotionEvent(float dx, float dy, float currentX, float currentY, + int displayId) { + if (!mController.isActivated(displayId)) { + // unrelated display. + return new float[]{dx, dy}; + } + + // TODO(361817142): implement centered and edge following types. + + // Continuous cursor following. + float scale = mController.getScale(displayId); + final float newCursorX = currentX + dx; + final float newCursorY = currentY + dy; + mController.setOffset(displayId, + newCursorX - newCursorX * scale, newCursorY - newCursorY * scale, + MAGNIFICATION_GESTURE_HANDLER_ID); + + // In the continuous mode, the cursor speed in physical display is kept. + // Thus, we don't consume any motion delta. + return new float[]{dx, dy}; + } +} diff --git a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java index ce7dcd0fa1d4..03107563ec22 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java @@ -299,14 +299,24 @@ public final class AssociationDiskStore { public void writeLastRemovedAssociation(AssociationInfo association, String reason) { Slog.i(TAG, "Writing last removed association=" + association.getId() + " to disk..."); + // Remove indirect identifier i.e. Mac Address + AssociationInfo.Builder builder = new AssociationInfo.Builder(association) + .setDeviceMacAddress(null); + // Set a placeholder display name if it's null because Mac Address and display name can't be + // both null. + if (association.getDisplayName() == null) { + builder.setDisplayName(""); + } + AssociationInfo redactedAssociation = builder.build(); + final AtomicFile file = createStorageFileForUser( - association.getUserId(), FILE_NAME_LAST_REMOVED_ASSOCIATION); + redactedAssociation.getUserId(), FILE_NAME_LAST_REMOVED_ASSOCIATION); writeToFileSafely(file, out -> { out.write(String.valueOf(System.currentTimeMillis()).getBytes()); out.write(' '); out.write(reason.getBytes()); out.write(' '); - out.write(association.toString().getBytes()); + out.write(redactedAssociation.toString().getBytes()); }); } diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java index 6c7c9b3e073d..4c62c0deb2df 100644 --- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java +++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java @@ -73,6 +73,8 @@ public class SecureChannel { private int mVerificationResult = FLAG_FAILURE_UNKNOWN; private boolean mPskVerified; + private final Object mHandshakeLock = new Object(); + /** * Create a new secure channel object. This secure channel allows secure messages to be @@ -342,20 +344,22 @@ public class SecureChannel { } private void initiateHandshake() throws IOException, BadHandleException , HandshakeException { - if (mConnectionContext != null) { - Slog.d(TAG, "Ukey2 handshake is already completed."); - return; - } + synchronized (mHandshakeLock) { + if (mConnectionContext != null) { + Slog.d(TAG, "Ukey2 handshake is already completed."); + return; + } - mRole = Role.INITIATOR; - mHandshakeContext = D2DHandshakeContext.forInitiator(); - mClientInit = mHandshakeContext.getNextHandshakeMessage(); + mRole = Role.INITIATOR; + mHandshakeContext = D2DHandshakeContext.forInitiator(); + mClientInit = mHandshakeContext.getNextHandshakeMessage(); - // Send Client Init - if (DEBUG) { - Slog.d(TAG, "Sending Ukey2 Client Init message"); + // Send Client Init + if (DEBUG) { + Slog.d(TAG, "Sending Ukey2 Client Init message"); + } + sendMessage(MessageType.HANDSHAKE_INIT, constructHandshakeInitMessage(mClientInit)); } - sendMessage(MessageType.HANDSHAKE_INIT, constructHandshakeInitMessage(mClientInit)); } // In an occasion where both participants try to initiate a handshake, resolve the conflict @@ -414,8 +418,17 @@ public class SecureChannel { // Mark "in-progress" upon receiving the first message mInProgress = true; + // Complete a series of handshake exchange and processing + synchronized (mHandshakeLock) { + completeHandshake(handshakeInitMessage); + } + } + + private void completeHandshake(byte[] initMessage) throws IOException, HandshakeException, + BadHandleException, CryptoException, AlertException { + // Handle a potential collision where both devices tried to initiate a connection - byte[] handshakeMessage = handleHandshakeCollision(handshakeInitMessage); + byte[] handshakeMessage = handleHandshakeCollision(initMessage); // Proceed with the rest of Ukey2 handshake if (mHandshakeContext == null) { // Server-side logic diff --git a/services/core/Android.bp b/services/core/Android.bp index 2aaf6a9c2391..14d9d3f0c0a1 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -121,6 +121,13 @@ genrule { out: ["com/android/server/location/contexthub/ContextHubStatsLog.java"], } +genrule { + name: "statslog-mediarouter-java-gen", + tools: ["stats-log-api-gen"], + cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog", + out: ["com/android/server/media/MediaRouterStatsLog.java"], +} + java_library_static { name: "services.core.unboosted", defaults: [ @@ -136,6 +143,7 @@ java_library_static { ":android.hardware.tv.mediaquality-V1-java-source", ":statslog-art-java-gen", ":statslog-contexthub-java-gen", + ":statslog-mediarouter-java-gen", ":services.core-aidl-sources", ":services.core-sources", ":services.core.protologsrc", diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 296f7cfe93ba..f54027ec0272 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -514,6 +514,7 @@ final class UiModeManagerService extends SystemService { mCarModeEnabled = mDockState == Intent.EXTRA_DOCK_STATE_CAR; registerVrStateListener(); // register listeners + // LINT.IfChange(fi_cb) context.getContentResolver() .registerContentObserver(Secure.getUriFor(Secure.UI_NIGHT_MODE), false, mDarkThemeObserver, 0); @@ -523,6 +524,7 @@ final class UiModeManagerService extends SystemService { Secure.getUriFor(ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED), false, mForceInvertStateObserver, UserHandle.USER_ALL); } + // LINT.ThenChange(/core/java/android/view/ViewRootImpl.java:fi_cb) context.getContentResolver().registerContentObserver( Secure.getUriFor(Secure.CONTRAST_LEVEL), false, mContrastObserver, UserHandle.USER_ALL); diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 600b124ffbf6..79fdcca9f75d 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -190,6 +190,7 @@ public class AccountManagerService } final Context mContext; + final PackageMonitor mPackageMonitor; private static final int[] INTERESTING_APP_OPS = new int[] { AppOpsManager.OP_GET_ACCOUNTS, @@ -373,7 +374,7 @@ public class AccountManagerService }, UserHandle.ALL, userFilter, null, null); // Need to cancel account request notifications if the update/install can access the account - new PackageMonitor() { + mPackageMonitor = new PackageMonitor() { @Override public void onPackageAdded(String packageName, int uid) { // Called on a handler, and running as the system @@ -397,7 +398,8 @@ public class AccountManagerService return; } } - }.register(mContext, mHandler.getLooper(), UserHandle.ALL, true); + }; + mPackageMonitor.register(mContext, mHandler.getLooper(), UserHandle.ALL, true); // Cancel account request notification if an app op was preventing the account access for (int i = 0; i < INTERESTING_APP_OPS.length; ++i) { diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 13d367a95942..336a35e7a7e3 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -3433,8 +3433,12 @@ public class OomAdjuster { // Process has user visible activities. return PROCESS_CAPABILITY_CPU_TIME; } - if (app.mServices.hasUndemotedShortForegroundService(nowUptime)) { - // It running a short fgs, just give it cpu time. + if (Flags.prototypeAggressiveFreezing()) { + if (app.mServices.hasUndemotedShortForegroundService(nowUptime)) { + // Grant cpu time for short FGS even when aggressively freezing. + return PROCESS_CAPABILITY_CPU_TIME; + } + } else if (app.mServices.hasForegroundServices()) { return PROCESS_CAPABILITY_CPU_TIME; } if (app.mReceivers.numberOfCurReceivers() > 0) { diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 2c0366e6a6db..ac30be99979e 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -31,7 +31,6 @@ import static android.os.Process.SYSTEM_UID; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.window.flags.Flags.balClearAllowlistDuration; import android.annotation.IntDef; import android.annotation.Nullable; @@ -330,7 +329,7 @@ public final class PendingIntentRecord extends IIntentSender.Stub { mAllowBgActivityStartsForActivitySender.remove(token); mAllowBgActivityStartsForBroadcastSender.remove(token); mAllowBgActivityStartsForServiceSender.remove(token); - if (mAllowlistDuration != null && balClearAllowlistDuration()) { + if (mAllowlistDuration != null) { TempAllowListDuration duration = mAllowlistDuration.get(token); if (duration != null && duration.type == TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED) { diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 1503d889c298..eea667ef2f39 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -1443,17 +1443,32 @@ class ProcessRecord implements WindowProcessListener { void onProcessFrozen() { mProfile.onProcessFrozen(); - if (mThread != null) mThread.onProcessPaused(); + final ApplicationThreadDeferred t; + synchronized (mService) { + t = mThread; + } + // Release the lock before calling the notifier, in case that calls back into AM. + if (t != null) t.onProcessPaused(); } void onProcessUnfrozen() { - if (mThread != null) mThread.onProcessUnpaused(); + final ApplicationThreadDeferred t; + synchronized (mService) { + t = mThread; + } + // Release the lock before calling the notifier, in case that calls back into AM. + if (t != null) t.onProcessUnpaused(); mProfile.onProcessUnfrozen(); mServices.onProcessUnfrozen(); } void onProcessFrozenCancelled() { - if (mThread != null) mThread.onProcessPausedCancelled(); + final ApplicationThreadDeferred t; + synchronized (mService) { + t = mThread; + } + // Release the lock before calling the notifier, in case that calls back into AM. + if (t != null) t.onProcessPausedCancelled(); mServices.onProcessFrozenCancelled(); } diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index 969a684f6ef5..2d387ea38df5 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -261,6 +261,7 @@ public class SyncManager { private final SyncLogger mLogger; private final AppCloningDeviceConfigHelper mAppCloningDeviceConfigHelper; + private final PackageMonitorImpl mPackageMonitor; private boolean isJobIdInUseLockedH(int jobId, List<JobInfo> pendingJobs) { for (int i = 0, size = pendingJobs.size(); i < size; i++) { @@ -725,8 +726,8 @@ public class SyncManager { mUserIntentReceiver, UserHandle.ALL, intentFilter, null, null); - final PackageMonitor packageMonitor = new PackageMonitorImpl(); - packageMonitor.register(mContext, null /* thread */, UserHandle.ALL, + mPackageMonitor = new PackageMonitorImpl(); + mPackageMonitor.register(mContext, null /* thread */, UserHandle.ALL, false /* externalStorage */); intentFilter = new IntentFilter(Intent.ACTION_TIME_CHANGED); diff --git a/services/core/java/com/android/server/display/DisplayAdapter.java b/services/core/java/com/android/server/display/DisplayAdapter.java index 69bc66fea56c..155f82a421ae 100644 --- a/services/core/java/com/android/server/display/DisplayAdapter.java +++ b/services/core/java/com/android/server/display/DisplayAdapter.java @@ -19,6 +19,7 @@ package com.android.server.display; import android.content.Context; import android.os.Handler; import android.view.Display; +import android.view.SurfaceControl; import com.android.server.display.feature.DisplayManagerFlags; @@ -138,6 +139,21 @@ abstract class DisplayAdapter { vsyncRate, /* isSynthetic= */ false, alternativeRefreshRates, supportedHdrTypes); } + static int getPowerModeForState(int state) { + switch (state) { + case Display.STATE_OFF: + return SurfaceControl.POWER_MODE_OFF; + case Display.STATE_DOZE: + return SurfaceControl.POWER_MODE_DOZE; + case Display.STATE_DOZE_SUSPEND: + return SurfaceControl.POWER_MODE_DOZE_SUSPEND; + case Display.STATE_ON_SUSPEND: + return SurfaceControl.POWER_MODE_ON_SUSPEND; + default: + return SurfaceControl.POWER_MODE_NORMAL; + } + } + public interface Listener { void onDisplayDeviceEvent(DisplayDevice device, int event); void onTraversalRequested(); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 258c95582e3a..d402f010281f 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2618,8 +2618,7 @@ public final class DisplayManagerService extends SystemService { // Blank or unblank the display immediately to match the state requested // by the display power controller (if known). DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); - if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0 - || android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device); if (display == null) { return null; @@ -5580,9 +5579,7 @@ public final class DisplayManagerService extends SystemService { final DisplayDevice displayDevice = mLogicalDisplayMapper.getDisplayLocked( id).getPrimaryDisplayDeviceLocked(); final int flags = displayDevice.getDisplayDeviceInfoLocked().flags; - if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0 - || android.companion.virtualdevice.flags.Flags - .correctVirtualDisplayPowerState()) { + if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(id); if (displayPowerController != null) { diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 551202c20cbb..7b714ad2bd9e 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -208,21 +208,6 @@ final class LocalDisplayAdapter extends DisplayAdapter { } } - static int getPowerModeForState(int state) { - switch (state) { - case Display.STATE_OFF: - return SurfaceControl.POWER_MODE_OFF; - case Display.STATE_DOZE: - return SurfaceControl.POWER_MODE_DOZE; - case Display.STATE_DOZE_SUSPEND: - return SurfaceControl.POWER_MODE_DOZE_SUSPEND; - case Display.STATE_ON_SUSPEND: - return SurfaceControl.POWER_MODE_ON_SUSPEND; - default: - return SurfaceControl.POWER_MODE_NORMAL; - } - } - private final class LocalDisplayDevice extends DisplayDevice { private final long mPhysicalDisplayId; private final SparseArray<DisplayModeRecord> mSupportedModes = new SparseArray<>(); diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index e7939bb50ece..ac03a93ca9e1 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -113,6 +113,11 @@ public class VirtualDisplayAdapter extends DisplayAdapter { public void destroyDisplay(IBinder displayToken) { DisplayControl.destroyVirtualDisplay(displayToken); } + + @Override + public void setDisplayPowerMode(IBinder displayToken, int mode) { + SurfaceControl.setDisplayPowerMode(displayToken, mode); + } }, featureFlags); } @@ -340,6 +345,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { private Display.Mode mMode; private int mDisplayIdToMirror; private boolean mIsWindowManagerMirroring; + private final boolean mNeverBlank; private final DisplayCutout mDisplayCutout; private final float mDefaultBrightness; private final float mDimBrightness; @@ -371,7 +377,11 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mCallback = callback; mProjection = projection; mMediaProjectionCallback = mediaProjectionCallback; - if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + // Private non-mirror displays are never blank and always on. + mNeverBlank = (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0 + && (flags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0; + if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState() + && !mNeverBlank) { // The display's power state depends on the power state of the state of its // display / power group, which we don't know here. Initializing to UNKNOWN allows // the first call to requestDisplayStateLocked() to set the correct state. @@ -471,7 +481,15 @@ public class VirtualDisplayAdapter extends DisplayAdapter { @Override public Runnable requestDisplayStateLocked(int state, float brightnessState, float sdrBrightnessState, DisplayOffloadSessionImpl displayOffloadSession) { + Runnable runnable = null; if (state != mDisplayState) { + Slog.d(TAG, "Changing state of virtual display " + mName + " from " + + Display.stateToString(mDisplayState) + " to " + + Display.stateToString(state)); + if (state != Display.STATE_ON && state != Display.STATE_OFF) { + Slog.wtf(TAG, "Unexpected display state for Virtual Display: " + + Display.stateToString(state)); + } mDisplayState = state; mInfo = null; sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED); @@ -480,6 +498,15 @@ public class VirtualDisplayAdapter extends DisplayAdapter { } else { mCallback.dispatchDisplayResumed(); } + + if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + final IBinder token = getDisplayTokenLocked(); + runnable = () -> { + final int mode = getPowerModeForState(state); + Slog.d(TAG, "Requesting power mode for display " + mName + " to " + mode); + mSurfaceControlDisplayFactory.setDisplayPowerMode(token, mode); + }; + } } if (android.companion.virtualdevice.flags.Flags.deviceAwareDisplayPower() && mBrightnessListener != null @@ -488,7 +515,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mCurrentBrightness = brightnessState; mCallback.dispatchRequestedBrightnessChanged(mCurrentBrightness); } - return null; + return runnable; } @Override @@ -572,23 +599,14 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mInfo.yDpi = mDensityDpi; mInfo.presentationDeadlineNanos = 1000000000L / (int) getRefreshRate(); // 1 frame mInfo.flags = 0; - if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { - if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { - mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE; - } - if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0) { - mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; - } - } else { - if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { - mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE - | DisplayDeviceInfo.FLAG_NEVER_BLANK; - } - if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { - mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK; - } else { - mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; - } + if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE; + } + if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + } + if (mNeverBlank) { + mInfo.flags |= DisplayDeviceInfo.FLAG_NEVER_BLANK; } if ((mFlags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) { mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP; @@ -782,5 +800,13 @@ public class VirtualDisplayAdapter extends DisplayAdapter { * @param displayToken The display token for the display to be destroyed. */ void destroyDisplay(IBinder displayToken); + + /** + * Set the display power mode in SurfaceFlinger. + * + * @param displayToken The display token for the display. + * @param mode the SurfaceControl power mode, e.g. {@link SurfaceControl#POWER_MODE_OFF}. + */ + void setDisplayPowerMode(IBinder displayToken, int mode); } } diff --git a/services/core/java/com/android/server/display/brightness/BrightnessReason.java b/services/core/java/com/android/server/display/brightness/BrightnessReason.java index a4804e1887fe..d4b9a6ce058b 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessReason.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessReason.java @@ -49,11 +49,9 @@ public final class BrightnessReason { public static final int MODIFIER_HDR = 0x4; public static final int MODIFIER_THROTTLED = 0x8; public static final int MODIFIER_MIN_LUX = 0x10; - public static final int MODIFIER_MIN_USER_SET_LOWER_BOUND = 0x20; - public static final int MODIFIER_STYLUS_UNDER_USE = 0x40; + public static final int MODIFIER_STYLUS_UNDER_USE = 0x20; public static final int MODIFIER_MASK = MODIFIER_DIMMED | MODIFIER_LOW_POWER | MODIFIER_HDR - | MODIFIER_THROTTLED | MODIFIER_MIN_LUX | MODIFIER_MIN_USER_SET_LOWER_BOUND - | MODIFIER_STYLUS_UNDER_USE; + | MODIFIER_THROTTLED | MODIFIER_MIN_LUX | MODIFIER_STYLUS_UNDER_USE; // ADJUSTMENT_* // These things can happen at any point, even if the main brightness reason doesn't @@ -157,9 +155,6 @@ public final class BrightnessReason { if ((mModifier & MODIFIER_MIN_LUX) != 0) { sb.append(" lux_lower_bound"); } - if ((mModifier & MODIFIER_MIN_USER_SET_LOWER_BOUND) != 0) { - sb.append(" user_min_pref"); - } if ((mModifier & MODIFIER_STYLUS_UNDER_USE) != 0) { sb.append(" stylus_under_use"); } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java index c3596c3e77fe..72cb31d8d1bb 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java @@ -19,12 +19,8 @@ package com.android.server.display.brightness.clamper; import android.content.ContentResolver; import android.content.Context; -import android.database.ContentObserver; import android.hardware.display.DisplayManagerInternal; -import android.net.Uri; import android.os.Handler; -import android.os.UserHandle; -import android.provider.Settings; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -49,7 +45,6 @@ public class BrightnessLowLuxModifier extends BrightnessModifier implements private static final String TAG = "BrightnessLowLuxModifier"; private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); private static final float MIN_NITS_DEFAULT = 0.2f; - private final SettingsObserver mSettingsObserver; private final ContentResolver mContentResolver; private final Handler mHandler; private final BrightnessClamperController.ClamperChangeListener mChangeListener; @@ -69,7 +64,6 @@ public class BrightnessLowLuxModifier extends BrightnessModifier implements mChangeListener = listener; mHandler = handler; mContentResolver = context.getContentResolver(); - mSettingsObserver = new SettingsObserver(mHandler); mDisplayDeviceConfig = displayDeviceConfig; mHandler.post(() -> { start(); @@ -82,12 +76,7 @@ public class BrightnessLowLuxModifier extends BrightnessModifier implements */ @VisibleForTesting public void recalculateLowerBound() { - float settingNitsLowerBound = Settings.Secure.getFloatForUser( - mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS, - /* def= */ MIN_NITS_DEFAULT, UserHandle.USER_CURRENT); - - boolean isActive = isSettingEnabled() - && mAmbientLux != BrightnessMappingStrategy.INVALID_LUX; + boolean isActive = mAmbientLux != BrightnessMappingStrategy.INVALID_LUX; final int reason; float minNitsAllowed = -1f; // undefined, if setting is off. @@ -95,12 +84,9 @@ public class BrightnessLowLuxModifier extends BrightnessModifier implements if (isActive) { float luxBasedNitsLowerBound = mDisplayDeviceConfig.getMinNitsFromLux(mAmbientLux); - minNitsAllowed = Math.max(settingNitsLowerBound, - luxBasedNitsLowerBound); + minNitsAllowed = Math.max(MIN_NITS_DEFAULT, luxBasedNitsLowerBound); minBrightnessAllowed = getBrightnessFromNits(minNitsAllowed); - reason = settingNitsLowerBound > luxBasedNitsLowerBound - ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND - : BrightnessReason.MODIFIER_MIN_LUX; + reason = BrightnessReason.MODIFIER_MIN_LUX; } else { minBrightnessAllowed = mDisplayDeviceConfig.getEvenDimmerTransitionPoint(); reason = 0; @@ -169,7 +155,6 @@ public class BrightnessLowLuxModifier extends BrightnessModifier implements @Override public void apply(DisplayManagerInternal.DisplayPowerRequest request, DisplayBrightnessState.Builder stateBuilder) { - stateBuilder.setMinBrightness(mBrightnessLowerBound); float boundedBrightness = Math.max(mBrightnessLowerBound, stateBuilder.getBrightness()); stateBuilder.setBrightness(boundedBrightness); @@ -181,12 +166,11 @@ public class BrightnessLowLuxModifier extends BrightnessModifier implements @Override public void stop() { - mContentResolver.unregisterContentObserver(mSettingsObserver); } @Override public boolean shouldListenToLightSensor() { - return isSettingEnabled(); + return true; } @Override @@ -204,37 +188,8 @@ public class BrightnessLowLuxModifier extends BrightnessModifier implements pw.println(" mMinNitsAllowed=" + mMinNitsAllowed); } - /** - * Defaults to true, on devices where setting is unset. - * - * @return if setting indicates feature is enabled - */ - private boolean isSettingEnabled() { - return Settings.Secure.getFloatForUser(mContentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, - /* def= */ 1.0f, UserHandle.USER_CURRENT) == 1.0f; - } - private float getBrightnessFromNits(float nits) { return mDisplayDeviceConfig.getBrightnessFromBacklight( mDisplayDeviceConfig.getBacklightFromNits(nits)); } - - private final class SettingsObserver extends ContentObserver { - - SettingsObserver(Handler handler) { - super(handler); - mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS), - false, this, UserHandle.USER_ALL); - mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED), - false, this, UserHandle.USER_ALL); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - recalculateLowerBound(); - } - } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 3cb21c3e2697..97f9a7c4f2b0 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -793,7 +793,7 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { @Override public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) { for (HdmiDeviceInfo info : deviceInfos) { - if (!isInputReady(info.getDeviceId())) { + if (!isInputReady(info.getId())) { mService.getHdmiCecNetwork().removeCecDevice( HdmiCecLocalDeviceTv.this, info.getLogicalAddress()); } diff --git a/services/core/java/com/android/server/infra/AbstractMasterSystemService.java b/services/core/java/com/android/server/infra/AbstractMasterSystemService.java index 6e5e0fd5224f..af065861f1ae 100644 --- a/services/core/java/com/android/server/infra/AbstractMasterSystemService.java +++ b/services/core/java/com/android/server/infra/AbstractMasterSystemService.java @@ -193,6 +193,8 @@ public abstract class AbstractMasterSystemService<M extends AbstractMasterSystem @Nullable private UserManagerInternal mUm; + private final MyPackageMonitor mPackageMonitor; + /** * Default constructor. * @@ -289,6 +291,7 @@ public abstract class AbstractMasterSystemService<M extends AbstractMasterSystem } }); } + mPackageMonitor = new MyPackageMonitor(/* supportsPackageRestartQuery */ true); startTrackingPackageChanges(); } @@ -986,260 +989,264 @@ public abstract class AbstractMasterSystemService<M extends AbstractMasterSystem } } - private void startTrackingPackageChanges() { - final PackageMonitor monitor = new PackageMonitor(true) { + private class MyPackageMonitor extends PackageMonitor { + MyPackageMonitor(boolean supportsPackageRestartQuery) { + super(supportsPackageRestartQuery); + } - @Override - public void onPackageUpdateStarted(@NonNull String packageName, int uid) { - if (verbose) Slog.v(mTag, "onPackageUpdateStarted(): " + packageName); + @Override + public void onPackageUpdateStarted(@NonNull String packageName, int uid) { + if (verbose) Slog.v(mTag, "onPackageUpdateStarted(): " + packageName); + synchronized (mLock) { final String activePackageName = getActiveServicePackageNameLocked(); if (!packageName.equals(activePackageName)) return; final int userId = getChangingUserId(); - synchronized (mLock) { - if (mUpdatingPackageNames == null) { - mUpdatingPackageNames = new SparseArray<String>(mServicesCacheList.size()); + + if (mUpdatingPackageNames == null) { + mUpdatingPackageNames = new SparseArray<String>(mServicesCacheList.size()); + } + mUpdatingPackageNames.put(userId, packageName); + onServicePackageUpdatingLocked(userId); + if ((mServicePackagePolicyFlags & PACKAGE_UPDATE_POLICY_NO_REFRESH) != 0) { + if (debug) { + Slog.d(mTag, "Holding service for user " + userId + " while package " + + activePackageName + " is being updated"); } - mUpdatingPackageNames.put(userId, packageName); - onServicePackageUpdatingLocked(userId); - if ((mServicePackagePolicyFlags & PACKAGE_UPDATE_POLICY_NO_REFRESH) != 0) { - if (debug) { - Slog.d(mTag, "Holding service for user " + userId + " while package " - + activePackageName + " is being updated"); - } - } else { - if (debug) { - Slog.d(mTag, "Removing service for user " + userId - + " because package " + activePackageName - + " is being updated"); - } - removeCachedServiceListLocked(userId); + } else { + if (debug) { + Slog.d(mTag, "Removing service for user " + userId + + " because package " + activePackageName + + " is being updated"); + } + removeCachedServiceListLocked(userId); - if ((mServicePackagePolicyFlags & PACKAGE_UPDATE_POLICY_REFRESH_EAGER) - != 0) { - if (debug) { - Slog.d(mTag, "Eagerly recreating service for user " - + userId); - } - getServiceForUserLocked(userId); + if ((mServicePackagePolicyFlags & PACKAGE_UPDATE_POLICY_REFRESH_EAGER) + != 0) { + if (debug) { + Slog.d(mTag, "Eagerly recreating service for user " + + userId); } + getServiceForUserLocked(userId); } } } + } - @Override - public void onPackageUpdateFinished(@NonNull String packageName, int uid) { - if (verbose) Slog.v(mTag, "onPackageUpdateFinished(): " + packageName); - final int userId = getChangingUserId(); - synchronized (mLock) { - final String activePackageName = mUpdatingPackageNames == null ? null - : mUpdatingPackageNames.get(userId); - if (packageName.equals(activePackageName)) { - if (mUpdatingPackageNames != null) { - mUpdatingPackageNames.remove(userId); - if (mUpdatingPackageNames.size() == 0) { - mUpdatingPackageNames = null; - } + @Override + public void onPackageUpdateFinished(@NonNull String packageName, int uid) { + if (verbose) Slog.v(mTag, "onPackageUpdateFinished(): " + packageName); + final int userId = getChangingUserId(); + synchronized (mLock) { + final String activePackageName = mUpdatingPackageNames == null ? null + : mUpdatingPackageNames.get(userId); + if (packageName.equals(activePackageName)) { + if (mUpdatingPackageNames != null) { + mUpdatingPackageNames.remove(userId); + if (mUpdatingPackageNames.size() == 0) { + mUpdatingPackageNames = null; } - onServicePackageUpdatedLocked(userId); - } else { - handlePackageUpdateLocked(packageName); } + onServicePackageUpdatedLocked(userId); + } else { + handlePackageUpdateLocked(packageName); } } + } - @Override - public void onPackageRemoved(String packageName, int uid) { - if (mServiceNameResolver != null - && mServiceNameResolver.isConfiguredInMultipleMode()) { - final int userId = getChangingUserId(); - synchronized (mLock) { - handlePackageRemovedMultiModeLocked(packageName, userId); - } - return; + @Override + public void onPackageRemoved(String packageName, int uid) { + if (mServiceNameResolver != null + && mServiceNameResolver.isConfiguredInMultipleMode()) { + final int userId = getChangingUserId(); + synchronized (mLock) { + handlePackageRemovedMultiModeLocked(packageName, userId); } + return; + } - synchronized (mLock) { - final int userId = getChangingUserId(); - final S service = peekServiceForUserLocked(userId); - if (service != null) { - final ComponentName componentName = service.getServiceComponentName(); - if (componentName != null) { - if (packageName.equals(componentName.getPackageName())) { - handleActiveServiceRemoved(userId); - } + synchronized (mLock) { + final int userId = getChangingUserId(); + final S service = peekServiceForUserLocked(userId); + if (service != null) { + final ComponentName componentName = service.getServiceComponentName(); + if (componentName != null) { + if (packageName.equals(componentName.getPackageName())) { + handleActiveServiceRemoved(userId); } } } } + } - @Override - public boolean onHandleForceStop(Intent intent, String[] packages, - int uid, boolean doit) { - synchronized (mLock) { - final String activePackageName = getActiveServicePackageNameLocked(); - for (String pkg : packages) { - if (pkg.equals(activePackageName)) { - if (!doit) { - return true; - } - final String action = intent.getAction(); - final int userId = getChangingUserId(); - if (Intent.ACTION_PACKAGE_RESTARTED.equals(action)) { - handleActiveServiceRestartedLocked(activePackageName, userId); - } else { - removeCachedServiceListLocked(userId); - } + @Override + public boolean onHandleForceStop(Intent intent, String[] packages, + int uid, boolean doit) { + synchronized (mLock) { + final String activePackageName = getActiveServicePackageNameLocked(); + for (String pkg : packages) { + if (pkg.equals(activePackageName)) { + if (!doit) { + return true; + } + final String action = intent.getAction(); + final int userId = getChangingUserId(); + if (Intent.ACTION_PACKAGE_RESTARTED.equals(action)) { + handleActiveServiceRestartedLocked(activePackageName, userId); } else { - handlePackageUpdateLocked(pkg); + removeCachedServiceListLocked(userId); } + } else { + handlePackageUpdateLocked(pkg); } } - return false; } + return false; + } - @Override - public void onPackageDataCleared(String packageName, int uid) { - if (verbose) Slog.v(mTag, "onPackageDataCleared(): " + packageName); - final int userId = getChangingUserId(); + @Override + public void onPackageDataCleared(String packageName, int uid) { + if (verbose) Slog.v(mTag, "onPackageDataCleared(): " + packageName); + final int userId = getChangingUserId(); - if (mServiceNameResolver != null - && mServiceNameResolver.isConfiguredInMultipleMode()) { - synchronized (mLock) { - onServicePackageDataClearedMultiModeLocked(packageName, userId); - } - return; + if (mServiceNameResolver != null + && mServiceNameResolver.isConfiguredInMultipleMode()) { + synchronized (mLock) { + onServicePackageDataClearedMultiModeLocked(packageName, userId); } + return; + } - synchronized (mLock) { - final S service = peekServiceForUserLocked(userId); - if (service != null) { - final ComponentName componentName = service.getServiceComponentName(); - if (componentName != null) { - if (packageName.equals(componentName.getPackageName())) { - onServicePackageDataClearedLocked(userId); - } + synchronized (mLock) { + final S service = peekServiceForUserLocked(userId); + if (service != null) { + final ComponentName componentName = service.getServiceComponentName(); + if (componentName != null) { + if (packageName.equals(componentName.getPackageName())) { + onServicePackageDataClearedLocked(userId); } } } } + } - private void handleActiveServiceRemoved(@UserIdInt int userId) { - synchronized (mLock) { - removeCachedServiceListLocked(userId); + private void handleActiveServiceRemoved(@UserIdInt int userId) { + synchronized (mLock) { + removeCachedServiceListLocked(userId); + } + final String serviceSettingsProperty = getServiceSettingsProperty(); + if (serviceSettingsProperty != null) { + Settings.Secure.putStringForUser(getContext().getContentResolver(), + serviceSettingsProperty, null, userId); + } + } + + @GuardedBy("mLock") + private void handleActiveServiceRestartedLocked(String activePackageName, + @UserIdInt int userId) { + if ((mServicePackagePolicyFlags & PACKAGE_RESTART_POLICY_NO_REFRESH) != 0) { + if (debug) { + Slog.d(mTag, "Holding service for user " + userId + " while package " + + activePackageName + " is being restarted"); } - final String serviceSettingsProperty = getServiceSettingsProperty(); - if (serviceSettingsProperty != null) { - Settings.Secure.putStringForUser(getContext().getContentResolver(), - serviceSettingsProperty, null, userId); + } else { + if (debug) { + Slog.d(mTag, "Removing service for user " + userId + + " because package " + activePackageName + + " is being restarted"); } - } + removeCachedServiceListLocked(userId); - @GuardedBy("mLock") - private void handleActiveServiceRestartedLocked(String activePackageName, - @UserIdInt int userId) { - if ((mServicePackagePolicyFlags & PACKAGE_RESTART_POLICY_NO_REFRESH) != 0) { + if ((mServicePackagePolicyFlags & PACKAGE_RESTART_POLICY_REFRESH_EAGER) != 0) { if (debug) { - Slog.d(mTag, "Holding service for user " + userId + " while package " - + activePackageName + " is being restarted"); - } - } else { - if (debug) { - Slog.d(mTag, "Removing service for user " + userId - + " because package " + activePackageName - + " is being restarted"); - } - removeCachedServiceListLocked(userId); - - if ((mServicePackagePolicyFlags & PACKAGE_RESTART_POLICY_REFRESH_EAGER) != 0) { - if (debug) { - Slog.d(mTag, "Eagerly recreating service for user " + userId); - } - updateCachedServiceLocked(userId); + Slog.d(mTag, "Eagerly recreating service for user " + userId); } + updateCachedServiceLocked(userId); } - onServicePackageRestartedLocked(userId); } + onServicePackageRestartedLocked(userId); + } - @Override - public void onPackageModified(String packageName) { - synchronized (mLock) { - if (verbose) Slog.v(mTag, "onPackageModified(): " + packageName); + @Override + public void onPackageModified(String packageName) { + synchronized (mLock) { + if (verbose) Slog.v(mTag, "onPackageModified(): " + packageName); - if (mServiceNameResolver == null) { - return; - } + if (mServiceNameResolver == null) { + return; + } - final int userId = getChangingUserId(); - final String[] serviceNames = mServiceNameResolver.getDefaultServiceNameList( - userId); - if (serviceNames != null) { - if (Flags.packageUpdateFixEnabled()) { - if (mServiceNameResolver.isConfiguredInMultipleMode()) { - // Remove any service that is in the cache but is no longer valid - // after this modification for this particular package - removeInvalidCachedServicesLocked(serviceNames, packageName, - userId); - } + final int userId = getChangingUserId(); + final String[] serviceNames = mServiceNameResolver.getDefaultServiceNameList( + userId); + if (serviceNames != null) { + if (Flags.packageUpdateFixEnabled()) { + if (mServiceNameResolver.isConfiguredInMultipleMode()) { + // Remove any service that is in the cache but is no longer valid + // after this modification for this particular package + removeInvalidCachedServicesLocked(serviceNames, packageName, + userId); } + } - // Update services that are still valid - for (int i = 0; i < serviceNames.length; i++) { - peekAndUpdateCachedServiceLocked(packageName, userId, - serviceNames[i]); - } + // Update services that are still valid + for (int i = 0; i < serviceNames.length; i++) { + peekAndUpdateCachedServiceLocked(packageName, userId, + serviceNames[i]); } } } + } - @GuardedBy("mLock") - private void peekAndUpdateCachedServiceLocked(String packageName, int userId, - String serviceName) { - if (serviceName == null) { - return; - } - - final ComponentName serviceComponentName = - ComponentName.unflattenFromString(serviceName); - if (serviceComponentName == null - || !serviceComponentName.getPackageName().equals(packageName)) { - return; - } + @GuardedBy("mLock") + private void peekAndUpdateCachedServiceLocked(String packageName, int userId, + String serviceName) { + if (serviceName == null) { + return; + } - // The default service package has changed, update the cached if the service - // exists but no active component. - final S service = peekServiceForUserLocked(userId); - if (service != null) { - final ComponentName componentName = service.getServiceComponentName(); - if (componentName == null) { - if (verbose) Slog.v(mTag, "update cached"); - updateCachedServiceLocked(userId); - } - } + final ComponentName serviceComponentName = + ComponentName.unflattenFromString(serviceName); + if (serviceComponentName == null + || !serviceComponentName.getPackageName().equals(packageName)) { + return; } - @GuardedBy("mLock") - private String getActiveServicePackageNameLocked() { - final int userId = getChangingUserId(); - final S service = peekServiceForUserLocked(userId); - if (service == null) { - return null; - } - final ComponentName serviceComponent = service.getServiceComponentName(); - if (serviceComponent == null) { - return null; + // The default service package has changed, update the cached if the service + // exists but no active component. + final S service = peekServiceForUserLocked(userId); + if (service != null) { + final ComponentName componentName = service.getServiceComponentName(); + if (componentName == null) { + if (verbose) Slog.v(mTag, "update cached"); + updateCachedServiceLocked(userId); } - return serviceComponent.getPackageName(); } + } - @GuardedBy("mLock") - private void handlePackageUpdateLocked(String packageName) { - visitServicesLocked((s) -> s.handlePackageUpdateLocked(packageName)); + @GuardedBy("mLock") + private String getActiveServicePackageNameLocked() { + final int userId = getChangingUserId(); + final S service = peekServiceForUserLocked(userId); + if (service == null) { + return null; + } + final ComponentName serviceComponent = service.getServiceComponentName(); + if (serviceComponent == null) { + return null; } - }; + return serviceComponent.getPackageName(); + } + @GuardedBy("mLock") + private void handlePackageUpdateLocked(String packageName) { + visitServicesLocked((s) -> s.handlePackageUpdateLocked(packageName)); + } + } + + private void startTrackingPackageChanges() { // package changes - monitor.register(getContext(), null, UserHandle.ALL, true); + mPackageMonitor.register(getContext(), null, UserHandle.ALL, true); } @GuardedBy("mLock") diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 7f853844c326..67e1ccc6a850 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -138,11 +138,6 @@ final class InputGestureManager { KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT ), createKeyGesture( - KeyEvent.KEYCODE_DEL, - KeyEvent.META_META_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_BACK - ), - createKeyGesture( KeyEvent.KEYCODE_ESCAPE, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_BACK diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index af726bd28718..68ad8f7e9433 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -2902,12 +2902,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. final long ident = Binder.clearCallingIdentity(); try { if (windowPerceptible != null && !windowPerceptible) { - if ((vis & InputMethodService.IME_VISIBLE) != 0) { - vis &= ~InputMethodService.IME_VISIBLE; - vis |= InputMethodService.IME_VISIBLE_IMPERCEPTIBLE; - } - } else { - vis &= ~InputMethodService.IME_VISIBLE_IMPERCEPTIBLE; + vis &= ~InputMethodService.IME_VISIBLE; } final var curId = bindingController.getCurId(); // TODO(b/305849394): Make mMenuController multi-user aware. diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index f40d0dd18213..2d937bdcc683 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -16,6 +16,8 @@ package com.android.server.location.contexthub; +import static com.android.server.location.contexthub.ContextHubTransactionManager.RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT; + import android.annotation.NonNull; import android.app.AppOpsManager; import android.content.Context; @@ -44,6 +46,9 @@ import com.android.internal.annotations.GuardedBy; import java.util.Collection; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; @@ -100,7 +105,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private final Object mOpenSessionLock = new Object(); - static class SessionInfo { + static class Session { enum SessionState { /* The session is pending acceptance from the remote endpoint. */ PENDING, @@ -119,7 +124,15 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub */ private final Set<Integer> mPendingSequenceNumbers = new HashSet<>(); - SessionInfo(HubEndpointInfo remoteEndpointInfo, boolean remoteInitiated) { + /** + * Stores the history of received messages that are timestamped. We use a LinkedHashMap to + * guarantee insertion ordering for easier manipulation of removing expired entries. + * + * <p>The key is the sequence number, and the value is the timestamp in milliseconds. + */ + private final LinkedHashMap<Integer, Long> mRxMessageHistoryMap = new LinkedHashMap<>(); + + Session(HubEndpointInfo remoteEndpointInfo, boolean remoteInitiated) { mRemoteEndpointInfo = remoteEndpointInfo; mRemoteInitiated = remoteInitiated; } @@ -157,11 +170,43 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub consumer.accept(sequenceNumber); } } + + public boolean isInMessageHistory(HubMessage message) { + // Clean up the history + Iterator<Map.Entry<Integer, Long>> iterator = + mRxMessageHistoryMap.entrySet().iterator(); + long nowMillis = System.currentTimeMillis(); + while (iterator.hasNext()) { + Map.Entry<Integer, Long> nextEntry = iterator.next(); + long expiryMillis = RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT.toMillis(); + if (nowMillis >= nextEntry.getValue() + expiryMillis) { + iterator.remove(); + } + break; + } + + return mRxMessageHistoryMap.containsKey(message.getMessageSequenceNumber()); + } + + public void addMessageToHistory(HubMessage message) { + if (mRxMessageHistoryMap.containsKey(message.getMessageSequenceNumber())) { + long value = mRxMessageHistoryMap.get(message.getMessageSequenceNumber()); + Log.w( + TAG, + "Message already exists in history (inserted @ " + + value + + " ms): " + + message); + return; + } + mRxMessageHistoryMap.put( + message.getMessageSequenceNumber(), System.currentTimeMillis()); + } } /** A map between a session ID which maps to its current state. */ @GuardedBy("mOpenSessionLock") - private final SparseArray<SessionInfo> mSessionInfoMap = new SparseArray<>(); + private final SparseArray<Session> mSessionMap = new SparseArray<>(); /** The package name of the app that created the endpoint */ private final String mPackageName; @@ -232,7 +277,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub synchronized (mOpenSessionLock) { try { - mSessionInfoMap.put(sessionId, new SessionInfo(destination, false)); + mSessionMap.put(sessionId, new Session(destination, false)); mHubInterface.openEndpointSession( sessionId, halEndpointInfo.id, mHalEndpointInfo.id, serviceDescriptor); } catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) { @@ -263,8 +308,8 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub super.unregister_enforcePermission(); synchronized (mOpenSessionLock) { // Iterate in reverse since cleanupSessionResources will remove the entry - for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) { - int id = mSessionInfoMap.keyAt(i); + for (int i = mSessionMap.size() - 1; i >= 0; i--) { + int id = mSessionMap.keyAt(i); halCloseEndpointSessionNoThrow(id, Reason.ENDPOINT_GONE); cleanupSessionResources(id); } @@ -290,14 +335,14 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub public void openSessionRequestComplete(int sessionId) { super.openSessionRequestComplete_enforcePermission(); synchronized (mOpenSessionLock) { - SessionInfo info = mSessionInfoMap.get(sessionId); + Session info = mSessionMap.get(sessionId); if (info == null) { throw new IllegalArgumentException( "openSessionRequestComplete for invalid session id=" + sessionId); } try { mHubInterface.endpointSessionOpenComplete(sessionId); - info.setSessionState(SessionInfo.SessionState.ACTIVE); + info.setSessionState(Session.SessionState.ACTIVE); } catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) { Log.e(TAG, "Exception while calling endpointSessionOpenComplete", e); } @@ -310,7 +355,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub int sessionId, HubMessage message, IContextHubTransactionCallback callback) { super.sendMessage_enforcePermission(); synchronized (mOpenSessionLock) { - SessionInfo info = mSessionInfoMap.get(sessionId); + Session info = mSessionMap.get(sessionId); if (info == null) { throw new IllegalArgumentException( "sendMessage for invalid session id=" + sessionId); @@ -393,9 +438,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } else { synchronized (mOpenSessionLock) { // Iterate in reverse since cleanupSessionResources will remove the entry - for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) { - int id = mSessionInfoMap.keyAt(i); - HubEndpointInfo target = mSessionInfoMap.get(id).getRemoteEndpointInfo(); + for (int i = mSessionMap.size() - 1; i >= 0; i--) { + int id = mSessionMap.keyAt(i); + HubEndpointInfo target = mSessionMap.get(id).getRemoteEndpointInfo(); if (!hasEndpointPermissions(target)) { halCloseEndpointSessionNoThrow(id, Reason.PERMISSION_DENIED); onCloseEndpointSession(id, Reason.PERMISSION_DENIED); @@ -415,13 +460,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub sb.append("wakelock: ").append(mWakeLock); } synchronized (mOpenSessionLock) { - if (mSessionInfoMap.size() != 0) { + if (mSessionMap.size() != 0) { sb.append(System.lineSeparator()); sb.append(" sessions: "); sb.append(System.lineSeparator()); } - for (int i = 0; i < mSessionInfoMap.size(); i++) { - int id = mSessionInfoMap.keyAt(i); + for (int i = 0; i < mSessionMap.size(); i++) { + int id = mSessionMap.keyAt(i); int count = i + 1; sb.append( " " @@ -429,7 +474,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + ". id=" + id + ", remote:" - + mSessionInfoMap.get(id).getRemoteEndpointInfo()); + + mSessionMap.get(id).getRemoteEndpointInfo()); sb.append(System.lineSeparator()); } } @@ -485,23 +530,23 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Log.w(TAG, "Unknown session ID in onEndpointSessionOpenComplete: id=" + sessionId); return; } - mSessionInfoMap.get(sessionId).setSessionState(SessionInfo.SessionState.ACTIVE); + mSessionMap.get(sessionId).setSessionState(Session.SessionState.ACTIVE); } invokeCallback((consumer) -> consumer.onSessionOpenComplete(sessionId)); } /* package */ void onMessageReceived(int sessionId, HubMessage message) { - byte code = onMessageReceivedInternal(sessionId, message); - if (code != ErrorCode.OK && message.isResponseRequired()) { - sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), code); + byte errorCode = onMessageReceivedInternal(sessionId, message); + if (errorCode != ErrorCode.OK && message.isResponseRequired()) { + sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), errorCode); } } /* package */ void onMessageDeliveryStatusReceived( int sessionId, int sequenceNumber, byte errorCode) { synchronized (mOpenSessionLock) { - SessionInfo info = mSessionInfoMap.get(sessionId); + Session info = mSessionMap.get(sessionId); if (info == null || !info.isActive()) { Log.w(TAG, "Received delivery status for invalid session: id=" + sessionId); return; @@ -517,7 +562,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /* package */ boolean hasSessionId(int sessionId) { synchronized (mOpenSessionLock) { - return mSessionInfoMap.contains(sessionId); + return mSessionMap.contains(sessionId); } } @@ -531,8 +576,8 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } synchronized (mOpenSessionLock) { - for (int i = mSessionInfoMap.size() - 1; i >= 0; i--) { - int id = mSessionInfoMap.keyAt(i); + for (int i = mSessionMap.size() - 1; i >= 0; i--) { + int id = mSessionMap.keyAt(i); onCloseEndpointSession(id, Reason.HUB_RESET); } } @@ -555,7 +600,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Log.e(TAG, "Existing session in onEndpointSessionOpenRequest: id=" + sessionId); return Optional.of(Reason.UNSPECIFIED); } - mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true)); + mSessionMap.put(sessionId, new Session(initiator, true)); } boolean success = @@ -567,7 +612,6 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } private byte onMessageReceivedInternal(int sessionId, HubMessage message) { - HubEndpointInfo remote; synchronized (mOpenSessionLock) { if (!isSessionActive(sessionId)) { Log.e( @@ -578,29 +622,36 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + message); return ErrorCode.PERMANENT_ERROR; } - remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo(); - } + HubEndpointInfo remote = mSessionMap.get(sessionId).getRemoteEndpointInfo(); + if (mSessionMap.get(sessionId).isInMessageHistory(message)) { + Log.e(TAG, "Dropping duplicate message: " + message); + return ErrorCode.TRANSIENT_ERROR; + } - try { - Binder.withCleanCallingIdentity( - () -> { - if (!notePermissions(remote)) { - throw new RuntimeException( - "Dropping message from " - + remote - + ". " - + mPackageName - + " doesn't have permission"); - } - }); - } catch (RuntimeException e) { - Log.e(TAG, e.getMessage()); - return ErrorCode.PERMISSION_DENIED; - } + try { + Binder.withCleanCallingIdentity( + () -> { + if (!notePermissions(remote)) { + throw new RuntimeException( + "Dropping message from " + + remote + + ". " + + mPackageName + + " doesn't have permission"); + } + }); + } catch (RuntimeException e) { + Log.e(TAG, e.getMessage()); + return ErrorCode.PERMISSION_DENIED; + } - boolean success = - invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message)); - return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR; + boolean success = + invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message)); + if (success) { + mSessionMap.get(sessionId).addMessageToHistory(message); + } + return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR; + } } /** @@ -634,7 +685,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub */ private boolean cleanupSessionResources(int sessionId) { synchronized (mOpenSessionLock) { - SessionInfo info = mSessionInfoMap.get(sessionId); + Session info = mSessionMap.get(sessionId); if (info != null) { if (!info.isRemoteInitiated()) { mEndpointManager.returnSessionId(sessionId); @@ -644,7 +695,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mTransactionManager.onMessageDeliveryResponse( sequenceNumber, /* success= */ false); }); - mSessionInfoMap.remove(sessionId); + mSessionMap.remove(sessionId); } return info != null; } @@ -656,7 +707,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub */ private boolean isSessionActive(int sessionId) { synchronized (mOpenSessionLock) { - return hasSessionId(sessionId) && mSessionInfoMap.get(sessionId).isActive(); + return hasSessionId(sessionId) && mSessionMap.get(sessionId).isActive(); } } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index 23e9ac5008f7..96e453963741 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -983,6 +983,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { Objects.requireNonNull(providerInfo, "providerInfo must not be null"); for (MediaRoute2Info route : providerInfo.getRoutes()) { + if (Flags.enableMirroringInMediaRouter2() + && route.supportsRemoteRouting() + && route.supportsSystemMediaRouting() + && route.getDeduplicationIds().isEmpty()) { + // This code is not accessible if the app is using the public API. + throw new SecurityException("Route is missing deduplication id: " + route); + } + if (route.isSystemRoute()) { throw new SecurityException( "Only the system is allowed to publish system routes. " diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index debac9436bb3..f137de1b3e1d 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -25,8 +25,20 @@ import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL; import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; - import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_RELEASE_SESSION; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_SELECT_ROUTE; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_TRANSFER_TO_ROUTE; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_COMMAND; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_ROUTE_ID; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_SESSION_ID; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_MANAGER_RECORD_NOT_FOUND; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_PERMISSION_DENIED; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTER_RECORD_NOT_FOUND; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_SUCCESS; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED; import android.Manifest; import android.annotation.NonNull; @@ -64,14 +76,12 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.util.SparseArray; - import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.pooled.PooledLambda; import com.android.media.flags.Flags; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; - import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -130,9 +140,14 @@ class MediaRouter2ServiceImpl { private final ArrayMap<IBinder, RouterRecord> mAllRouterRecords = new ArrayMap<>(); @GuardedBy("mLock") private final ArrayMap<IBinder, ManagerRecord> mAllManagerRecords = new ArrayMap<>(); + @GuardedBy("mLock") private int mCurrentActiveUserId = -1; + @GuardedBy("mLock") + private static final MediaRouterMetricLogger mMediaRouterMetricLogger = + new MediaRouterMetricLogger(); + private final ActivityManager.OnUidImportanceListener mOnUidImportanceListener = (uid, importance) -> { synchronized (mLock) { @@ -350,8 +365,8 @@ class MediaRouter2ServiceImpl { } } - public void setDiscoveryRequestWithRouter2(@NonNull IMediaRouter2 router, - @NonNull RouteDiscoveryPreference preference) { + public void setDiscoveryRequestWithRouter2( + @NonNull IMediaRouter2 router, @NonNull RouteDiscoveryPreference preference) { Objects.requireNonNull(router, "router must not be null"); Objects.requireNonNull(preference, "preference must not be null"); @@ -409,8 +424,8 @@ class MediaRouter2ServiceImpl { } } - public void setRouteVolumeWithRouter2(@NonNull IMediaRouter2 router, - @NonNull MediaRoute2Info route, int volume) { + public void setRouteVolumeWithRouter2( + @NonNull IMediaRouter2 router, @NonNull MediaRoute2Info route, int volume) { Objects.requireNonNull(router, "router must not be null"); Objects.requireNonNull(route, "route must not be null"); @@ -439,12 +454,7 @@ class MediaRouter2ServiceImpl { try { synchronized (mLock) { requestCreateSessionWithRouter2Locked( - requestId, - managerRequestId, - router, - oldSession, - route, - sessionHints); + requestId, managerRequestId, router, oldSession, route, sessionHints); } } finally { Binder.restoreCallingIdentity(token); @@ -1326,6 +1336,9 @@ class MediaRouter2ServiceImpl { final RouterRecord routerRecord = mAllRouterRecords.get(binder); if (routerRecord == null) { + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTER_RECORD_NOT_FOUND); return; } @@ -1344,15 +1357,22 @@ class MediaRouter2ServiceImpl { if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) { ManagerRecord manager = userHandler.findManagerWithId(toRequesterId(managerRequestId)); if (manager == null || manager.mLastSessionCreationRequest == null) { - Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " - + "Ignoring unknown request."); + Slog.w(TAG, "requestCreateSessionWithRouter2Locked: Ignoring unknown request."); + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_MANAGER_RECORD_NOT_FOUND); routerRecord.notifySessionCreationFailed(requestId); return; } - if (!TextUtils.equals(manager.mLastSessionCreationRequest.mOldSession.getId(), - oldSession.getId())) { - Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " - + "Ignoring unmatched routing session."); + if (!TextUtils.equals( + manager.mLastSessionCreationRequest.mOldSession.getId(), oldSession.getId())) { + Slog.w( + TAG, + "requestCreateSessionWithRouter2Locked: " + + "Ignoring unmatched routing session."); + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_SESSION_ID); routerRecord.notifySessionCreationFailed(requestId); return; } @@ -1364,8 +1384,13 @@ class MediaRouter2ServiceImpl { && route.isSystemRoute()) { route = manager.mLastSessionCreationRequest.mRoute; } else { - Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " - + "Ignoring unmatched route."); + Slog.w( + TAG, + "requestCreateSessionWithRouter2Locked: " + + "Ignoring unmatched route."); + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_ROUTE_ID); routerRecord.notifySessionCreationFailed(requestId); return; } @@ -1376,14 +1401,19 @@ class MediaRouter2ServiceImpl { if (route.isSystemRoute() && !routerRecord.hasSystemRoutingPermission() && !TextUtils.equals(route.getId(), defaultRouteId)) { - Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to" - + route); + Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to" + route); + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_PERMISSION_DENIED); routerRecord.notifySessionCreationFailed(requestId); return; } } long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId); + mMediaRouterMetricLogger.addRequestInfo( + uniqueRequestId, + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION); userHandler.sendMessage( obtainMessage( UserHandler::requestCreateSessionWithRouter2OnHandler, @@ -1403,6 +1433,9 @@ class MediaRouter2ServiceImpl { final RouterRecord routerRecord = mAllRouterRecords.get(binder); if (routerRecord == null) { + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_SELECT_ROUTE, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTER_RECORD_NOT_FOUND); return; } @@ -1411,6 +1444,9 @@ class MediaRouter2ServiceImpl { TextUtils.formatSimple( "selectRouteWithRouter2 | router: %s(id: %d), route: %s", routerRecord.mPackageName, routerRecord.mRouterId, route.getId())); + mMediaRouterMetricLogger.logOperationTriggered( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_SELECT_ROUTE, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED); routerRecord.mUserRecord.mHandler.sendMessage( obtainMessage(UserHandler::selectRouteOnHandler, @@ -1425,6 +1461,9 @@ class MediaRouter2ServiceImpl { final RouterRecord routerRecord = mAllRouterRecords.get(binder); if (routerRecord == null) { + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTER_RECORD_NOT_FOUND); return; } @@ -1433,6 +1472,9 @@ class MediaRouter2ServiceImpl { TextUtils.formatSimple( "deselectRouteWithRouter2 | router: %s(id: %d), route: %s", routerRecord.mPackageName, routerRecord.mRouterId, route.getId())); + mMediaRouterMetricLogger.logOperationTriggered( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED); routerRecord.mUserRecord.mHandler.sendMessage( obtainMessage(UserHandler::deselectRouteOnHandler, @@ -1450,6 +1492,9 @@ class MediaRouter2ServiceImpl { final RouterRecord routerRecord = mAllRouterRecords.get(binder); if (routerRecord == null) { + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_TRANSFER_TO_ROUTE, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTER_RECORD_NOT_FOUND); return; } @@ -1458,6 +1503,9 @@ class MediaRouter2ServiceImpl { TextUtils.formatSimple( "transferToRouteWithRouter2 | router: %s(id: %d), route: %s", routerRecord.mPackageName, routerRecord.mRouterId, route.getId())); + mMediaRouterMetricLogger.logOperationTriggered( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_TRANSFER_TO_ROUTE, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED); UserHandler userHandler = routerRecord.mUserRecord.mHandler; String defaultRouteId = userHandler.getSystemProvider().getDefaultRoute().getId(); @@ -1516,6 +1564,9 @@ class MediaRouter2ServiceImpl { final RouterRecord routerRecord = mAllRouterRecords.get(binder); if (routerRecord == null) { + mMediaRouterMetricLogger.logOperationFailure( + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_RELEASE_SESSION, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTER_RECORD_NOT_FOUND); return; } @@ -1794,6 +1845,9 @@ class MediaRouter2ServiceImpl { .findRouterWithSessionLocked(uniqueSessionId); long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); + mMediaRouterMetricLogger.addRequestInfo( + uniqueRequestId, MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_SELECT_ROUTE); + managerRecord.mUserRecord.mHandler.sendMessage( obtainMessage(UserHandler::selectRouteOnHandler, managerRecord.mUserRecord.mHandler, @@ -1820,6 +1874,10 @@ class MediaRouter2ServiceImpl { .findRouterWithSessionLocked(uniqueSessionId); long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); + mMediaRouterMetricLogger.addRequestInfo( + uniqueRequestId, + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE); + managerRecord.mUserRecord.mHandler.sendMessage( obtainMessage(UserHandler::deselectRouteOnHandler, managerRecord.mUserRecord.mHandler, @@ -1851,6 +1909,10 @@ class MediaRouter2ServiceImpl { .findRouterWithSessionLocked(uniqueSessionId); long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); + mMediaRouterMetricLogger.addRequestInfo( + uniqueRequestId, + MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_TRANSFER_TO_ROUTE); + managerRecord.mUserRecord.mHandler.sendMessage( obtainMessage( UserHandler::transferToRouteOnHandler, @@ -2792,7 +2854,8 @@ class MediaRouter2ServiceImpl { if (!addedRoutes.isEmpty()) { // If routes were added, newInfo cannot be null. - Slog.i(TAG, + Slog.i( + TAG, toLoggingMessage( /* source= */ "addProviderRoutes", newInfo.getUniqueId(), @@ -2954,7 +3017,7 @@ class MediaRouter2ServiceImpl { private void selectRouteOnHandler(long uniqueRequestId, @Nullable RouterRecord routerRecord, @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route, - "selecting")) { + "selecting", uniqueRequestId)) { return; } @@ -2963,8 +3026,12 @@ class MediaRouter2ServiceImpl { if (provider == null) { return; } - provider.selectRoute(uniqueRequestId, getOriginalId(uniqueSessionId), - route.getOriginalId()); + provider.selectRoute( + uniqueRequestId, getOriginalId(uniqueSessionId), route.getOriginalId()); + + // Log the success result. + mMediaRouterMetricLogger.logRequestResult( + uniqueRequestId, MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_SUCCESS); } // routerRecord can be null if the session is system's or RCN. @@ -2972,7 +3039,7 @@ class MediaRouter2ServiceImpl { @Nullable RouterRecord routerRecord, @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route, - "deselecting")) { + "deselecting", uniqueRequestId)) { return; } @@ -2982,8 +3049,12 @@ class MediaRouter2ServiceImpl { return; } - provider.deselectRoute(uniqueRequestId, getOriginalId(uniqueSessionId), - route.getOriginalId()); + provider.deselectRoute( + uniqueRequestId, getOriginalId(uniqueSessionId), route.getOriginalId()); + + // Log the success result. + mMediaRouterMetricLogger.logRequestResult( + uniqueRequestId, MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_SUCCESS); } // routerRecord can be null if the session is system's or RCN. @@ -2996,7 +3067,7 @@ class MediaRouter2ServiceImpl { @NonNull MediaRoute2Info route, @RoutingSessionInfo.TransferReason int transferReason) { if (!checkArgumentsForSessionControl(routerRecord, uniqueSessionId, route, - "transferring to")) { + "transferring to", uniqueRequestId)) { return; } @@ -3016,18 +3087,25 @@ class MediaRouter2ServiceImpl { getOriginalId(uniqueSessionId), route.getOriginalId(), transferReason); + + // Log the success result. + mMediaRouterMetricLogger.logRequestResult( + uniqueRequestId, MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_SUCCESS); } // routerRecord is null if and only if the session is created without the request, which // includes the system's session and RCN cases. private boolean checkArgumentsForSessionControl(@Nullable RouterRecord routerRecord, @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route, - @NonNull String description) { + @NonNull String description, long uniqueRequestId) { final String providerId = route.getProviderId(); final MediaRoute2Provider provider = findProvider(providerId); if (provider == null) { Slog.w(TAG, "Ignoring " + description + " route since no provider found for " + "given route=" + route); + mMediaRouterMetricLogger.logRequestResult( + uniqueRequestId, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_COMMAND); return false; } @@ -3050,6 +3128,9 @@ class MediaRouter2ServiceImpl { + getPackageNameFromNullableRecord(matchingRecord) + " route=" + route); + mMediaRouterMetricLogger.logRequestResult( + uniqueRequestId, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTER_RECORD_NOT_FOUND); return false; } @@ -3057,6 +3138,9 @@ class MediaRouter2ServiceImpl { if (sessionId == null) { Slog.w(TAG, "Failed to get original session id from unique session id. " + "uniqueSessionId=" + uniqueSessionId); + mMediaRouterMetricLogger.logRequestResult( + uniqueRequestId, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_SESSION_ID); return false; } @@ -3168,6 +3252,10 @@ class MediaRouter2ServiceImpl { } matchingRequest.mRouterRecord.notifySessionCreated( toOriginalRequestId(uniqueRequestId), sessionInfo); + + // Log the success result. + mMediaRouterMetricLogger.logRequestResult( + uniqueRequestId, MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_SUCCESS); } /** @@ -3255,10 +3343,14 @@ class MediaRouter2ServiceImpl { // Currently, only manager records can get notified of failures. // TODO(b/282936553): Notify regular routers of request failures. + + // Log the request result. + mMediaRouterMetricLogger.logRequestResult( + uniqueRequestId, MediaRouterMetricLogger.convertResultFromReason(reason)); } - private boolean handleSessionCreationRequestFailed(@NonNull MediaRoute2Provider provider, - long uniqueRequestId, int reason) { + private boolean handleSessionCreationRequestFailed( + @NonNull MediaRoute2Provider provider, long uniqueRequestId, int reason) { // Check whether the failure is about creating a session SessionCreationRequest matchingRequest = null; for (SessionCreationRequest request : mSessionCreationRequests) { @@ -3385,8 +3477,8 @@ class MediaRouter2ServiceImpl { } } - private void notifySessionCreatedToManagers(long managerRequestId, - @NonNull RoutingSessionInfo session) { + private void notifySessionCreatedToManagers( + long managerRequestId, @NonNull RoutingSessionInfo session) { int requesterId = toRequesterId(managerRequestId); int originalRequestId = toOriginalRequestId(managerRequestId); diff --git a/services/core/java/com/android/server/media/MediaRouterMetricLogger.java b/services/core/java/com/android/server/media/MediaRouterMetricLogger.java new file mode 100644 index 000000000000..56d2a1b22254 --- /dev/null +++ b/services/core/java/com/android/server/media/MediaRouterMetricLogger.java @@ -0,0 +1,219 @@ +/* + * Copyright 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; + +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_FAILED_TO_REROUTE_SYSTEM_MEDIA; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_COMMAND; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_NETWORK_ERROR; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_REJECTED; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTE_NOT_AVAILABLE; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNIMPLEMENTED; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNKNOWN_ERROR; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED; + +import android.annotation.NonNull; +import android.media.MediaRoute2ProviderService; +import android.util.Log; +import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; +import java.io.PrintWriter; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Logs metrics for MediaRouter2. + * + * @hide + */ +final class MediaRouterMetricLogger { + private static final String TAG = "MediaRouterMetricLogger"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final int REQUEST_INFO_CACHE_CAPACITY = 100; + + /** LRU cache to store request info. */ + private final RequestInfoCache mRequestInfoCache; + + /** Constructor for {@link MediaRouterMetricLogger}. */ + public MediaRouterMetricLogger() { + mRequestInfoCache = new RequestInfoCache(REQUEST_INFO_CACHE_CAPACITY); + } + + /** + * Adds a new request info to the cache. + * + * @param uniqueRequestId The unique request id. + * @param eventType The event type. + */ + public void addRequestInfo(long uniqueRequestId, int eventType) { + RequestInfo requestInfo = new RequestInfo(uniqueRequestId, eventType); + mRequestInfoCache.put(requestInfo.mUniqueRequestId, requestInfo); + } + + /** + * Removes a request info from the cache. + * + * @param uniqueRequestId The unique request id. + */ + public void removeRequestInfo(long uniqueRequestId) { + mRequestInfoCache.remove(uniqueRequestId); + } + + /** + * Logs an operation failure. + * + * @param eventType The event type. + * @param result The result of the operation. + */ + public void logOperationFailure(int eventType, int result) { + logMediaRouterEvent(eventType, result); + } + + /** + * Logs an operation triggered. + * + * @param eventType The event type. + */ + public void logOperationTriggered(int eventType, int result) { + logMediaRouterEvent(eventType, result); + } + + /** + * Logs the result of a request. + * + * @param uniqueRequestId The unique request id. + * @param result The result of the request. + */ + public void logRequestResult(long uniqueRequestId, int result) { + RequestInfo requestInfo = mRequestInfoCache.get(uniqueRequestId); + if (requestInfo == null) { + Slog.w( + TAG, + "logRequestResult: No RequestInfo found for uniqueRequestId=" + + uniqueRequestId); + return; + } + + int eventType = requestInfo.mEventType; + logMediaRouterEvent(eventType, result); + + removeRequestInfo(uniqueRequestId); + } + + /** + * Converts a reason code from {@link MediaRoute2ProviderService} to a result code for logging. + * + * @param reason The reason code from {@link MediaRoute2ProviderService}. + * @return The result code for logging. + */ + public static int convertResultFromReason(int reason) { + switch (reason) { + case MediaRoute2ProviderService.REASON_UNKNOWN_ERROR: + return MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNKNOWN_ERROR; + case MediaRoute2ProviderService.REASON_REJECTED: + return MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_REJECTED; + case MediaRoute2ProviderService.REASON_NETWORK_ERROR: + return MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_NETWORK_ERROR; + case MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE: + return MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTE_NOT_AVAILABLE; + case MediaRoute2ProviderService.REASON_INVALID_COMMAND: + return MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_COMMAND; + case MediaRoute2ProviderService.REASON_UNIMPLEMENTED: + return MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNIMPLEMENTED; + case MediaRoute2ProviderService.REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA: + return MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_FAILED_TO_REROUTE_SYSTEM_MEDIA; + default: + return MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED; + } + } + + /** + * Gets the size of the request info cache. + * + * @return The size of the request info cache. + */ + @VisibleForTesting + public int getRequestCacheSize() { + return mRequestInfoCache.size(); + } + + private void logMediaRouterEvent(int eventType, int result) { + MediaRouterStatsLog.write( + MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED, eventType, result); + + if (DEBUG) { + Slog.d(TAG, "logMediaRouterEvent: " + eventType + " " + result); + } + } + + /** A cache for storing request info that evicts entries when it reaches its capacity. */ + class RequestInfoCache extends LinkedHashMap<Long, RequestInfo> { + + public final int capacity; + + /** + * Constructor for {@link RequestInfoCache}. + * + * @param capacity The maximum capacity of the cache. + */ + public RequestInfoCache(int capacity) { + super(capacity, 1.0f, true); + this.capacity = capacity; + } + + @Override + protected boolean removeEldestEntry(Map.Entry<Long, RequestInfo> eldest) { + boolean shouldRemove = size() > capacity; + if (shouldRemove) { + Slog.d(TAG, "Evicted request info: " + eldest.getValue()); + logOperationTriggered( + eldest.getValue().mEventType, + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED); + } + return shouldRemove; + } + } + + /** Class to store request info. */ + static class RequestInfo { + public final long mUniqueRequestId; + public final int mEventType; + + /** + * Constructor for {@link RequestInfo}. + * + * @param uniqueRequestId The unique request id. + * @param eventType The event type. + */ + RequestInfo(long uniqueRequestId, int eventType) { + mUniqueRequestId = uniqueRequestId; + mEventType = eventType; + } + + /** + * Dumps the request info. + * + * @param pw The print writer. + * @param prefix The prefix for the output. + */ + public void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + pw.println(prefix + "RequestInfo"); + String indent = prefix + " "; + pw.println(indent + "mUniqueRequestId=" + mUniqueRequestId); + pw.println(indent + "mEventType=" + mEventType); + } + } +} diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 60371d751c4a..0f1d28db8d82 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2809,7 +2809,6 @@ public class NotificationManagerService extends SystemService { mNotificationChannelLogger, mAppOps, mUserProfiles, - mUgmInternal, mShowReviewPermissionsNotification, Clock.systemUTC()); mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper, @@ -7210,7 +7209,13 @@ public class NotificationManagerService extends SystemService { final Uri originalSoundUri = (originalChannel != null) ? originalChannel.getSound() : null; if (soundUri != null && !Objects.equals(originalSoundUri, soundUri)) { - PermissionHelper.grantUriPermission(mUgmInternal, soundUri, sourceUid); + Binder.withCleanCallingIdentity(() -> { + mUgmInternal.checkGrantUriPermission(sourceUid, null, + ContentProvider.getUriWithoutUserId(soundUri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(soundUri, + UserHandle.getUserId(sourceUid))); + }); } } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 5a58f457ba08..cec5a93a2a15 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -37,7 +37,10 @@ import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.Person; +import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ShortcutInfo; @@ -46,6 +49,7 @@ import android.media.AudioAttributes; import android.media.AudioSystem; import android.metrics.LogMaker; import android.net.Uri; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -813,7 +817,13 @@ public final class NotificationRecord { } if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization()) && signals.containsKey(KEY_SUMMARIZATION)) { - mSummarization = signals.getString(KEY_SUMMARIZATION); + CharSequence summary = signals.getCharSequence(KEY_SUMMARIZATION, + signals.getString(KEY_SUMMARIZATION)); + if (summary != null) { + mSummarization = summary.toString(); + } else { + mSummarization = null; + } EventLogTags.writeNotificationAdjusted(getKey(), KEY_SUMMARIZATION, Boolean.toString(mSummarization != null)); } @@ -1532,15 +1542,21 @@ public final class NotificationRecord { * {@link #mGrantableUris}. Otherwise, this will either log or throw * {@link SecurityException} depending on target SDK of enqueuing app. */ - private void visitGrantableUri(Uri uri, boolean userOverriddenUri, - boolean isSound) { + private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { + if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; + if (mGrantableUris != null && mGrantableUris.contains(uri)) { return; // already verified this URI } final int sourceUid = getSbn().getUid(); + final long ident = Binder.clearCallingIdentity(); try { - PermissionHelper.grantUriPermission(mUgmInternal, uri, sourceUid); + // This will throw a SecurityException if the caller can't grant. + mUgmInternal.checkGrantUriPermission(sourceUid, null, + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); if (mGrantableUris == null) { mGrantableUris = new ArraySet<>(); @@ -1560,6 +1576,8 @@ public final class NotificationRecord { } } } + } finally { + Binder.restoreCallingIdentity(ident); } } diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java index 1464d481311a..b6f48890c528 100644 --- a/services/core/java/com/android/server/notification/PermissionHelper.java +++ b/services/core/java/com/android/server/notification/PermissionHelper.java @@ -25,25 +25,19 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.companion.virtual.VirtualDeviceManager; -import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; -import android.net.Uri; import android.os.Binder; import android.os.RemoteException; -import android.os.UserHandle; import android.permission.IPermissionManager; import android.util.ArrayMap; import android.util.Pair; import android.util.Slog; import com.android.internal.util.ArrayUtils; -import com.android.server.uri.UriGrantsManagerInternal; import java.util.Collections; import java.util.HashSet; @@ -64,7 +58,7 @@ public final class PermissionHelper { private final IPermissionManager mPermManager; public PermissionHelper(Context context, IPackageManager packageManager, - IPermissionManager permManager) { + IPermissionManager permManager) { mContext = context; mPackageManager = packageManager; mPermManager = permManager; @@ -304,19 +298,6 @@ public final class PermissionHelper { return false; } - static void grantUriPermission(final UriGrantsManagerInternal ugmInternal, Uri uri, - int sourceUid) { - if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; - - Binder.withCleanCallingIdentity(() -> { - // This will throw a SecurityException if the caller can't grant. - ugmInternal.checkGrantUriPermission(sourceUid, null, - ContentProvider.getUriWithoutUserId(uri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); - }); - } - public static class PackagePermission { public final String packageName; public final @UserIdInt int userId; diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 46ff7983bb2d..b26b4571b07a 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -100,7 +100,6 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.notification.PermissionHelper.PackagePermission; -import com.android.server.uri.UriGrantsManagerInternal; import org.json.JSONArray; import org.json.JSONException; @@ -228,7 +227,6 @@ public class PreferencesHelper implements RankingConfig { private final NotificationChannelLogger mNotificationChannelLogger; private final AppOpsManager mAppOps; private final ManagedServices.UserProfiles mUserProfiles; - private final UriGrantsManagerInternal mUgmInternal; private SparseBooleanArray mBadgingEnabled; private SparseBooleanArray mBubblesEnabled; @@ -247,7 +245,6 @@ public class PreferencesHelper implements RankingConfig { ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager, NotificationChannelLogger notificationChannelLogger, AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles, - UriGrantsManagerInternal ugmInternal, boolean showReviewPermissionsNotification, Clock clock) { mContext = context; mZenModeHelper = zenHelper; @@ -258,7 +255,6 @@ public class PreferencesHelper implements RankingConfig { mNotificationChannelLogger = notificationChannelLogger; mAppOps = appOpsManager; mUserProfiles = userProfiles; - mUgmInternal = ugmInternal; mShowReviewPermissionsNotification = showReviewPermissionsNotification; mIsMediaNotificationFilteringEnabled = context.getResources() .getBoolean(R.bool.config_quickSettingsShowMediaPlayer); @@ -1195,11 +1191,6 @@ public class PreferencesHelper implements RankingConfig { } clearLockedFieldsLocked(channel); - // Verify that the app has permission to read the sound Uri - // Only check for new channels, as regular apps can only set sound - // before creating. See: {@link NotificationChannel#setSound} - PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); - channel.setImportanceLockedByCriticalDeviceFunction( r.defaultAppLockedImportance || r.fixedImportance); diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index 60d028b46970..f3797614dee0 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -392,7 +392,7 @@ public class BackgroundInstallControlService extends SystemService { private boolean installedByAdb(String initiatingPackageName) { // GTS tests needs to adopt shell identity to install apps. - if(!SystemProperties.get("gts.transparency.bg-install-apps").isEmpty()) { + if(!SystemProperties.get("debug.gts.transparency.bg-install-apps").isEmpty()) { Slog.d(TAG, "handlePackageAdd: is GTS tests, skipping ADB check"); } else if(PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName)) { Slog.d(TAG, "handlePackageAdd: is installed by ADB, skipping"); diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 635ef069741b..af788ea6ccdb 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -21,6 +21,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_INSTA import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_UPDATED_BY_DO; import static android.content.pm.DataLoaderType.INCREMENTAL; import static android.content.pm.DataLoaderType.STREAMING; +import static android.content.pm.Flags.cloudCompilationVerification; import static android.content.pm.PackageInstaller.LOCATION_DATA_APP; import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; @@ -3687,6 +3688,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { CollectionUtils.addAll(stagedSplitTypes, apk.getSplitTypes()); } + if (cloudCompilationVerification()) { + verifySdmSignatures(artManagedFilePaths, mSigningDetails); + } + if (removeSplitList.size() > 0) { if (pkgInfo == null) { throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, @@ -4028,6 +4033,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { File targetArtManagedFile = new File( ArtManagedInstallFileHelper.getTargetPathForApk(path, targetFile.getPath())); stageFileLocked(artManagedFile, targetArtManagedFile); + if (!artManagedFile.equals(targetArtManagedFile)) { + // The file has been renamed. Update the list to reflect the change. + for (int i = 0; i < artManagedFilePaths.size(); ++i) { + if (artManagedFilePaths.get(i).equals(path)) { + artManagedFilePaths.set(i, targetArtManagedFile.getAbsolutePath()); + } + } + } } } @@ -4309,6 +4322,37 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } /** + * Verifies the signatures of SDM files. + * + * SDM is a file format that contains the cloud compilation artifacts. As a requirement, the SDM + * file should be signed with the same key as the APK. + * + * TODO(b/377474232): Move this logic to ART Service. + */ + private static void verifySdmSignatures(List<String> artManagedFilePaths, + SigningDetails expectedSigningDetails) throws PackageManagerException { + ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + for (String path : artManagedFilePaths) { + if (!path.endsWith(".sdm")) { + continue; + } + // SDM is a format introduced in Android 16, so we don't need to support older + // signature schemes. + int minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3; + ParseResult<SigningDetails> verified = + ApkSignatureVerifier.verify(input, path, minSignatureScheme); + if (verified.isError()) { + throw new PackageManagerException( + INSTALL_FAILED_INVALID_APK, "Failed to verify SDM signatures"); + } + if (!expectedSigningDetails.signaturesMatchExactly(verified.getResult())) { + throw new PackageManagerException( + INSTALL_FAILED_INVALID_APK, "SDM signatures are inconsistent with APK"); + } + } + } + + /** * @return the uid of the owner this session */ public int getInstallerUid() { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index d3513053caf3..66e9e772e063 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -1792,7 +1792,7 @@ public class ShortcutService extends IShortcutService.Stub { void injectPostToHandlerDebounced(@NonNull final Object token, @NonNull final Runnable r) { Objects.requireNonNull(token); Objects.requireNonNull(r); - synchronized (mServiceLock) { + synchronized (mHandler) { mHandler.removeCallbacksAndMessages(token); mHandler.postDelayed(r, token, CALLBACK_DELAY); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 22f20028eb9c..46dc75817a36 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3801,7 +3801,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } } - // fall through + break; case KeyEvent.KEYCODE_ESCAPE: if (firstDown && event.isMetaPressed()) { notifyKeyGestureCompleted(event, diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 58534b95bdde..1299a4d86623 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -294,19 +294,6 @@ final class AccessibilityController { } } - void onAppWindowTransition(int displayId, int transition) { - if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { - mAccessibilityTracing.logTrace(TAG + ".onAppWindowTransition", - FLAGS_MAGNIFICATION_CALLBACK, - "displayId=" + displayId + "; transition=" + transition); - } - final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId); - if (displayMagnifier != null) { - displayMagnifier.onAppWindowTransition(displayId, transition); - } - // Not relevant for the window observer. - } - void onWMTransition(int displayId, @TransitionType int type, @TransitionFlags int flags) { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace(TAG + ".onWMTransition", @@ -670,34 +657,6 @@ final class AccessibilityController { mHandler.sendEmptyMessage(MyHandler.MESSAGE_NOTIFY_DISPLAY_SIZE_CHANGED); } - void onAppWindowTransition(int displayId, int transition) { - if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { - mAccessibilityTracing.logTrace(LOG_TAG + ".onAppWindowTransition", - FLAGS_MAGNIFICATION_CALLBACK, - "displayId=" + displayId + "; transition=" + transition); - } - if (DEBUG_WINDOW_TRANSITIONS) { - Slog.i(LOG_TAG, "Window transition: " - + AppTransition.appTransitionOldToString(transition) - + " displayId: " + displayId); - } - final boolean isMagnifierActivated = isFullscreenMagnificationActivated(); - if (!isMagnifierActivated) { - return; - } - switch (transition) { - case WindowManager.TRANSIT_OLD_ACTIVITY_OPEN: - case WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN: - case WindowManager.TRANSIT_OLD_TASK_OPEN: - case WindowManager.TRANSIT_OLD_TASK_TO_FRONT: - case WindowManager.TRANSIT_OLD_WALLPAPER_OPEN: - case WindowManager.TRANSIT_OLD_WALLPAPER_CLOSE: - case WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN: { - mUserContextChangedNotifier.onAppWindowTransition(transition); - } - } - } - void onWMTransition(int displayId, @TransitionType int type, @TransitionFlags int flags) { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace(LOG_TAG + ".onWMTransition", @@ -734,7 +693,7 @@ final class AccessibilityController { } if (DEBUG_WINDOW_TRANSITIONS) { Slog.i(LOG_TAG, "Window transition: " - + AppTransition.appTransitionOldToString(transition) + + WindowManager.transitTypeToString(transition) + " displayId: " + windowState.getDisplayId()); } final boolean isMagnifierActivated = isFullscreenMagnificationActivated(); diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 463a830c9e68..7da4beb95114 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -1113,11 +1113,11 @@ class ActivityClientController extends IActivityClientController.Stub { false /* fromClient */); } + final EnterPipRequestedItem item = new EnterPipRequestedItem(r.token); try { - final EnterPipRequestedItem item = new EnterPipRequestedItem(r.token); - mService.getLifecycleManager().scheduleTransactionItem(r.app.getThread(), item); - return true; - } catch (Exception e) { + return mService.getLifecycleManager().scheduleTransactionItem(r.app.getThread(), item); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "Failed to send enter pip requested item: " + r.intent.getComponent(), e); return false; @@ -1129,10 +1129,11 @@ class ActivityClientController extends IActivityClientController.Stub { */ void onPictureInPictureUiStateChanged(@NonNull ActivityRecord r, PictureInPictureUiState pipState) { + final PipStateTransactionItem item = new PipStateTransactionItem(r.token, pipState); try { - final PipStateTransactionItem item = new PipStateTransactionItem(r.token, pipState); mService.getLifecycleManager().scheduleTransactionItem(r.app.getThread(), item); - } catch (Exception e) { + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "Failed to send pip state transaction item: " + r.intent.getComponent(), e); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index d452d76db18d..3cd4db7d8dfc 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -253,6 +253,7 @@ import android.annotation.Size; import android.app.Activity; import android.app.ActivityManager.TaskDescription; import android.app.ActivityOptions; +import android.app.IApplicationThread; import android.app.IScreenCaptureObserver; import android.app.PendingIntent; import android.app.PictureInPictureParams; @@ -672,9 +673,6 @@ final class ActivityRecord extends WindowToken { // TODO(b/317000737): Replace it with visibility states lookup. int mTransitionChangeFlags; - /** Whether we need to setup the animation to animate only within the letterbox. */ - private boolean mNeedsLetterboxedAnimation; - /** * @see #currentLaunchCanTurnScreenOn() */ @@ -1351,15 +1349,16 @@ final class ActivityRecord extends WindowToken { this, displayId); return; } - try { - ProtoLog.v(WM_DEBUG_SWITCH, "Reporting activity moved to " - + "display, activityRecord=%s, displayId=%d, config=%s", this, displayId, - config); + ProtoLog.v(WM_DEBUG_SWITCH, "Reporting activity moved to " + + "display, activityRecord=%s, displayId=%d, config=%s", this, displayId, + config); - final MoveToDisplayItem item = - new MoveToDisplayItem(token, displayId, config, activityWindowInfo); + final MoveToDisplayItem item = + new MoveToDisplayItem(token, displayId, config, activityWindowInfo); + try { mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup // If process died, whatever. } } @@ -1371,14 +1370,15 @@ final class ActivityRecord extends WindowToken { + "update - client not running, activityRecord=%s", this); return; } - try { - ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending new config to %s, " - + "config: %s", this, config); + ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending new config to %s, " + + "config: %s", this, config); - final ActivityConfigurationChangeItem item = - new ActivityConfigurationChangeItem(token, config, activityWindowInfo); + final ActivityConfigurationChangeItem item = + new ActivityConfigurationChangeItem(token, config, activityWindowInfo); + try { mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup // If process died, whatever. } } @@ -1393,19 +1393,18 @@ final class ActivityRecord extends WindowToken { if (onTop) { app.addToPendingTop(); } - try { - ProtoLog.v(WM_DEBUG_STATES, "Sending position change to %s, onTop: %b", - this, onTop); + ProtoLog.v(WM_DEBUG_STATES, "Sending position change to %s, onTop: %b", + this, onTop); - final TopResumedActivityChangeItem item = - new TopResumedActivityChangeItem(token, onTop); - mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); + final TopResumedActivityChangeItem item = new TopResumedActivityChangeItem(token, onTop); + try { + return mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup // If process died, whatever. Slog.w(TAG, "Failed to send top-resumed=" + onTop + " to " + this, e); return false; } - return true; } void updateMultiWindowMode() { @@ -2604,14 +2603,21 @@ final class ActivityRecord extends WindowToken { removeStartingWindow(); return; } + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT; + final TransferSplashScreenViewStateItem item = + new TransferSplashScreenViewStateItem(token, parcelable, windowAnimationLeash); + boolean isSuccessful; try { - mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT; - final TransferSplashScreenViewStateItem item = - new TransferSplashScreenViewStateItem(token, parcelable, windowAnimationLeash); - mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); - scheduleTransferSplashScreenTimeout(); - } catch (Exception e) { + isSuccessful = mAtmService.getLifecycleManager().scheduleTransactionItem( + app.getThread(), item); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "onCopySplashScreenComplete fail: " + this); + isSuccessful = false; + } + if (isSuccessful) { + scheduleTransferSplashScreenTimeout(); + } else { mStartingWindow.cancelAnimation(); parcelable.clearIfNeeded(); mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH; @@ -3957,11 +3963,23 @@ final class ActivityRecord extends WindowToken { boolean skipDestroy = false; - try { - if (DEBUG_SWITCH) Slog.i(TAG_SWITCH, "Destroying: " + this); + if (DEBUG_SWITCH) Slog.i(TAG_SWITCH, "Destroying: " + this); + boolean isSuccessful; + final IApplicationThread client = app.getThread(); + if (client == null) { + Slog.w(TAG_WM, "Failed to schedule DestroyActivityItem because client is inactive"); + isSuccessful = false; + } else { final DestroyActivityItem item = new DestroyActivityItem(token, finishing); - mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); - } catch (Exception e) { + try { + isSuccessful = mAtmService.getLifecycleManager().scheduleTransactionItem( + client, item); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup + isSuccessful = false; + } + } + if (!isSuccessful) { // We can just ignore exceptions here... if the process has crashed, our death // notification will clean things up. if (finishing) { @@ -4884,13 +4902,17 @@ final class ActivityRecord extends WindowToken { } if (isState(RESUMED) && attachedToProcess()) { + final ArrayList<ResultInfo> list = new ArrayList<>(); + list.add(new ResultInfo(resultWho, requestCode, resultCode, data, callerToken)); + final ActivityResultItem item = new ActivityResultItem(token, list); try { - final ArrayList<ResultInfo> list = new ArrayList<>(); - list.add(new ResultInfo(resultWho, requestCode, resultCode, data, callerToken)); - final ActivityResultItem item = new ActivityResultItem(token, list); - mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); - return; - } catch (Exception e) { + final boolean isSuccessful = mAtmService.getLifecycleManager() + .scheduleTransactionItem(app.getThread(), item); + if (isSuccessful) { + return; + } + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "Exception thrown sending result to " + this, e); } } @@ -4917,6 +4939,7 @@ final class ActivityRecord extends WindowToken { app.getThread(), activityResultItem); } } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "Exception thrown sending result to " + this, e); } // We return here to ensure that result for media projection setup is not stored as a @@ -4989,7 +5012,6 @@ final class ActivityRecord extends WindowToken { } final ReferrerIntent rintent = new ReferrerIntent(intent, getFilteredReferrer(referrer), callerToken); - boolean unsent = true; final boolean isTopActivityWhileSleeping = isSleeping() && isTopRunningActivity(); // We want to immediately deliver the intent to the activity if: @@ -4998,25 +5020,26 @@ final class ActivityRecord extends WindowToken { // - The device is sleeping and it is the top activity behind the lock screen (b/6700897). if ((mState == RESUMED || mState == PAUSED || isTopActivityWhileSleeping) && attachedToProcess()) { + final ArrayList<ReferrerIntent> ar = new ArrayList<>(1); + ar.add(rintent); + // Making sure the client state is RESUMED after transaction completed and doing + // so only if activity is currently RESUMED. Otherwise, client may have extra + // life-cycle calls to RESUMED (and PAUSED later). + final NewIntentItem item = new NewIntentItem(token, ar, mState == RESUMED /* resume */); try { - ArrayList<ReferrerIntent> ar = new ArrayList<>(1); - ar.add(rintent); - // Making sure the client state is RESUMED after transaction completed and doing - // so only if activity is currently RESUMED. Otherwise, client may have extra - // life-cycle calls to RESUMED (and PAUSED later). - final NewIntentItem item = - new NewIntentItem(token, ar, mState == RESUMED /* resume */); - mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); - unsent = false; + final boolean isSuccessful = mAtmService.getLifecycleManager() + .scheduleTransactionItem(app.getThread(), item); + if (isSuccessful) { + return; + } } catch (RemoteException e) { - Slog.w(TAG, "Exception thrown sending new intent to " + this, e); - } catch (NullPointerException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "Exception thrown sending new intent to " + this, e); } } - if (unsent) { - addNewIntentLocked(rintent); - } + + // Didn't send. + addNewIntentLocked(rintent); } void updateOptionsLocked(ActivityOptions options) { @@ -5580,18 +5603,6 @@ final class ActivityRecord extends WindowToken { commitVisibility(visible, performLayout, false /* fromTransition */); } - void setNeedsLetterboxedAnimation(boolean needsLetterboxedAnimation) { - mNeedsLetterboxedAnimation = needsLetterboxedAnimation; - } - - boolean isNeedsLetterboxedAnimation() { - return mNeedsLetterboxedAnimation; - } - - boolean isInLetterboxAnimation() { - return mNeedsLetterboxedAnimation && isAnimating(); - } - /** Updates draw state and shows drawn windows. */ void commitFinishDrawing(SurfaceControl.Transaction t) { boolean committed = false; @@ -6044,11 +6055,12 @@ final class ActivityRecord extends WindowToken { setState(PAUSING, "makeActiveIfNeeded"); EventLogTags.writeWmPauseActivity(mUserId, System.identityHashCode(this), shortComponentName, "userLeaving=false", "make-active"); + final PauseActivityItem item = new PauseActivityItem(token, finishing, + false /* userLeaving */, false /* dontReport */, mAutoEnteringPip); try { - final PauseActivityItem item = new PauseActivityItem(token, finishing, - false /* userLeaving */, false /* dontReport */, mAutoEnteringPip); mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); - } catch (Exception e) { + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "Exception thrown sending pause: " + intent.getComponent(), e); } } else if (shouldStartActivity()) { @@ -6057,10 +6069,11 @@ final class ActivityRecord extends WindowToken { } setState(STARTED, "makeActiveIfNeeded"); + final StartActivityItem item = new StartActivityItem(token, takeSceneTransitionInfo()); try { - mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), - new StartActivityItem(token, takeSceneTransitionInfo())); - } catch (Exception e) { + mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "Exception thrown sending start: " + intent.getComponent(), e); } // The activity may be waiting for stop, but that is no longer appropriate if we are @@ -6343,23 +6356,29 @@ final class ActivityRecord extends WindowToken { return; } resumeKeyDispatchingLocked(); - try { - ProtoLog.v(WM_DEBUG_STATES, "Moving to STOPPING: %s (stop requested)", this); + ProtoLog.v(WM_DEBUG_STATES, "Moving to STOPPING: %s (stop requested)", this); - setState(STOPPING, "stopIfPossible"); - if (DEBUG_VISIBILITY) { - Slog.v(TAG_VISIBILITY, "Stopping:" + this); - } - EventLogTags.writeWmStopActivity( - mUserId, System.identityHashCode(this), shortComponentName); - mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), - new StopActivityItem(token)); - - mAtmService.mH.postDelayed(mStopTimeoutRunnable, STOP_TIMEOUT); - } catch (Exception e) { + setState(STOPPING, "stopIfPossible"); + if (DEBUG_VISIBILITY) { + Slog.v(TAG_VISIBILITY, "Stopping:" + this); + } + EventLogTags.writeWmStopActivity( + mUserId, System.identityHashCode(this), shortComponentName); + final StopActivityItem item = new StopActivityItem(token); + boolean isSuccessful; + try { + isSuccessful = mAtmService.getLifecycleManager().scheduleTransactionItem( + app.getThread(), item); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup // Maybe just ignore exceptions here... if the process has crashed, our death // notification will clean things up. Slog.w(TAG, "Exception thrown during pause", e); + isSuccessful = false; + } + if (isSuccessful) { + mAtmService.mH.postDelayed(mStopTimeoutRunnable, STOP_TIMEOUT); + } else { // Just in case, assume it to be stopped. mAppStopped = true; mStoppedTime = SystemClock.uptimeMillis(); @@ -7253,10 +7272,6 @@ final class ActivityRecord extends WindowToken { .setParent(getAnimationLeashParent()) .setName(getSurfaceControl() + " - animation-bounds") .setCallsite("ActivityRecord.createAnimationBoundsLayer"); - if (mNeedsLetterboxedAnimation) { - // Needs to be an effect layer to support rounded corners - builder.setEffectLayer(); - } final SurfaceControl boundsLayer = builder.build(); t.show(boundsLayer); return boundsLayer; @@ -7274,11 +7289,6 @@ final class ActivityRecord extends WindowToken { @Override public void onLeashAnimationStarting(Transaction t, SurfaceControl leash) { - if (mNeedsLetterboxedAnimation) { - updateLetterboxSurfaceIfNeeded(findMainWindow(), t); - mNeedsAnimationBoundsLayer = true; - } - // If the animation needs to be cropped then an animation bounds layer is created as a // child of the root pinned task or animation layer. The leash is then reparented to this // new layer. @@ -7291,17 +7301,6 @@ final class ActivityRecord extends WindowToken { t.setLayer(leash, 0); t.setLayer(mAnimationBoundsLayer, getLastLayer()); - if (mNeedsLetterboxedAnimation) { - final int cornerRadius = mAppCompatController.getLetterboxPolicy() - .getRoundedCornersRadius(findMainWindow()); - - final Rect letterboxInnerBounds = new Rect(); - getLetterboxInnerBounds(letterboxInnerBounds); - - t.setCornerRadius(mAnimationBoundsLayer, cornerRadius) - .setCrop(mAnimationBoundsLayer, letterboxInnerBounds); - } - // Reparent leash to animation bounds layer. t.reparent(leash, mAnimationBoundsLayer); } @@ -7355,10 +7354,6 @@ final class ActivityRecord extends WindowToken { } mNeedsAnimationBoundsLayer = false; - if (mNeedsLetterboxedAnimation) { - mNeedsLetterboxedAnimation = false; - updateLetterboxSurfaceIfNeeded(findMainWindow(), t); - } } @Override @@ -8711,9 +8706,11 @@ final class ActivityRecord extends WindowToken { } // Figure out how to handle the changes between the configurations. - ProtoLog.v(WM_DEBUG_CONFIGURATION, "Checking to restart %s: changed=0x%s, " - + "handles=0x%s, mLastReportedConfiguration=%s", info.name, - Integer.toHexString(changes), Integer.toHexString(info.getRealConfigChanged()), + ProtoLog.v(WM_DEBUG_CONFIGURATION, "Checking to restart %s: changed=%s, " + + "handles=%s, not-handles=%s, mLastReportedConfiguration=%s", info.name, + Configuration.configurationDiffToString(changes), + Configuration.configurationDiffToString(info.getRealConfigChanged()), + Configuration.configurationDiffToString(changes & ~(info.getRealConfigChanged())), mLastReportedConfiguration); if (shouldRelaunchLocked(changes, mTmpConfig)) { @@ -8926,29 +8923,34 @@ final class ActivityRecord extends WindowToken { task.mTaskId, shortComponentName, Integer.toHexString(configChangeFlags)); } + ProtoLog.i(WM_DEBUG_STATES, "Moving to %s Relaunching %s callers=%s" , + (andResume ? "RESUMED" : "PAUSED"), this, Debug.getCallers(6)); + final ClientTransactionItem callbackItem = new ActivityRelaunchItem(token, + pendingResults, pendingNewIntents, configChangeFlags, + new MergedConfiguration(getProcessGlobalConfiguration(), + getMergedOverrideConfiguration()), + preserveWindow, getActivityWindowInfo()); + final ActivityLifecycleItem lifecycleItem; + if (andResume) { + lifecycleItem = new ResumeActivityItem(token, isTransitionForward(), + shouldSendCompatFakeFocus()); + } else { + lifecycleItem = new PauseActivityItem(token); + } + boolean isSuccessful; try { - ProtoLog.i(WM_DEBUG_STATES, "Moving to %s Relaunching %s callers=%s" , - (andResume ? "RESUMED" : "PAUSED"), this, Debug.getCallers(6)); - final ClientTransactionItem callbackItem = new ActivityRelaunchItem(token, - pendingResults, pendingNewIntents, configChangeFlags, - new MergedConfiguration(getProcessGlobalConfiguration(), - getMergedOverrideConfiguration()), - preserveWindow, getActivityWindowInfo()); - final ActivityLifecycleItem lifecycleItem; - if (andResume) { - lifecycleItem = new ResumeActivityItem(token, isTransitionForward(), - shouldSendCompatFakeFocus()); - } else { - lifecycleItem = new PauseActivityItem(token); - } - mAtmService.getLifecycleManager().scheduleTransactionItems( + isSuccessful = mAtmService.getLifecycleManager().scheduleTransactionItems( app.getThread(), callbackItem, lifecycleItem); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup + Slog.w(TAG, "Failed to relaunch " + this + ": " + e); + isSuccessful = false; + } + if (isSuccessful) { startRelaunching(); // Note: don't need to call pauseIfSleepingLocked() here, because the caller will only // request resume if this activity is currently resumed, which implies we aren't // sleeping. - } catch (RemoteException e) { - Slog.w(TAG, "Failed to relaunch " + this + ": " + e); } if (andResume) { @@ -9028,10 +9030,11 @@ final class ActivityRecord extends WindowToken { private void scheduleStopForRestartProcess() { // The process will be killed until the activity reports stopped with saved state (see // {@link ActivityTaskManagerService.activityStopped}). + final StopActivityItem item = new StopActivityItem(token); try { - mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), - new StopActivityItem(token)); + mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), item); } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup Slog.w(TAG, "Exception thrown during restart " + this, e); } mTaskSupervisor.scheduleRestartTimeout(this); diff --git a/services/core/java/com/android/server/wm/ActivityRefresher.java b/services/core/java/com/android/server/wm/ActivityRefresher.java index 25e38b307b5e..8fe603cad46b 100644 --- a/services/core/java/com/android/server/wm/ActivityRefresher.java +++ b/services/core/java/com/android/server/wm/ActivityRefresher.java @@ -88,17 +88,22 @@ class ActivityRefresher { new RefreshCallbackItem(activity.token, cycleThroughStop ? ON_STOP : ON_PAUSE); final ResumeActivityItem resumeActivityItem = new ResumeActivityItem( activity.token, /* isForward */ false, /* shouldSendCompatFakeFocus */ false); + boolean isSuccessful; try { - activity.mAtmService.getLifecycleManager().scheduleTransactionItems( + isSuccessful = activity.mAtmService.getLifecycleManager().scheduleTransactionItems( activity.app.getThread(), refreshCallbackItem, resumeActivityItem); + } catch (RemoteException e) { + isSuccessful = false; + } + if (isSuccessful) { mHandler.postDelayed(() -> { synchronized (mWmService.mGlobalLock) { onActivityRefreshed(activity); } }, REFRESH_CALLBACK_TIMEOUT_MS); - } catch (RemoteException e) { - activity.mAppCompatController.getCameraOverrides() - .setIsRefreshRequested(false); + } else { + activity.mAppCompatController.getCameraOverrides().setIsRefreshRequested(false); + } } diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java index 6873270366ee..27511b2f1668 100644 --- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java @@ -217,7 +217,7 @@ class AppCompatLetterboxPolicy { } final boolean shouldShowLetterboxUi = - (mActivityRecord.isInLetterboxAnimation() || mActivityRecord.isVisible() + (mActivityRecord.isVisible() || mActivityRecord.isVisibleRequested()) && mainWindow.areAppWindowBoundsLetterboxed() // Check for FLAG_SHOW_WALLPAPER explicitly instead of using @@ -360,8 +360,7 @@ class AppCompatLetterboxPolicy { .mAppCompatController.getReachabilityPolicy(); mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), mActivityRecord.mWmService.mTransactionFactory, - reachabilityPolicy, letterboxOverrides, - this::getLetterboxParentSurface); + reachabilityPolicy, letterboxOverrides); mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame); } @@ -469,15 +468,6 @@ class AppCompatLetterboxPolicy { public boolean isFullyTransparentBarAllowed(@NonNull Rect rect) { return !isRunning() || mLetterbox.notIntersectsOrFullyContains(rect); } - - @Nullable - private SurfaceControl getLetterboxParentSurface() { - if (mActivityRecord.isInLetterboxAnimation()) { - return mActivityRecord.getTask().getSurfaceControl(); - } - return mActivityRecord.getSurfaceControl(); - } - } /** diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxUtils.java b/services/core/java/com/android/server/wm/AppCompatLetterboxUtils.java index 83d7cb7bb960..e5b61db2ef68 100644 --- a/services/core/java/com/android/server/wm/AppCompatLetterboxUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxUtils.java @@ -36,12 +36,7 @@ class AppCompatLetterboxUtils { outLetterboxPosition.set(0, 0); return; } - if (activity.isInLetterboxAnimation()) { - // In this case we attach the letterbox to the task instead of the activity. - activity.getTask().getPosition(outLetterboxPosition); - } else { - activity.getPosition(outLetterboxPosition); - } + activity.getPosition(outLetterboxPosition); } /** diff --git a/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java b/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java index 8165638d4bda..92b91464312e 100644 --- a/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java +++ b/services/core/java/com/android/server/wm/AppCompatRoundedCorners.java @@ -58,7 +58,7 @@ class AppCompatRoundedCorners { @VisibleForTesting @Nullable Rect getCropBoundsIfNeeded(@NonNull final WindowState mainWindow) { - if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) { + if (!requiresRoundedCorners(mainWindow)) { // We don't want corner radius on the window. // In the case the ActivityRecord requires a letterboxed animation we never want // rounded corners on the window because rounded corners are applied at the diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java deleted file mode 100644 index 12d4a210400c..000000000000 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ /dev/null @@ -1,1587 +0,0 @@ -/* - * Copyright (C) 2011 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.view.WindowManager.LayoutParams; -import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_LAUNCHER_CLEAR_SNAPSHOT; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER; -import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND; -import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; -import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_NONE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_RELAUNCH; -import static android.view.WindowManager.TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE; -import static android.view.WindowManager.TRANSIT_OLD_NONE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN_BEHIND; -import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_BACK; -import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_FRONT; -import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_UNSET; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_OPEN; -import static android.view.WindowManager.TRANSIT_OPEN; -import static android.view.WindowManager.TRANSIT_RELAUNCH; -import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; - -import static com.android.internal.R.styleable.WindowAnimation_activityCloseEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_activityCloseExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_activityOpenEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_activityOpenExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_dreamActivityCloseExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_dreamActivityOpenEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_dreamActivityOpenExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_launchTaskBehindSourceAnimation; -import static com.android.internal.R.styleable.WindowAnimation_launchTaskBehindTargetAnimation; -import static com.android.internal.R.styleable.WindowAnimation_taskCloseEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_taskCloseExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_taskOpenEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_taskOpenExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_taskToBackEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_taskToBackExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_taskToFrontEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_taskToFrontExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_wallpaperCloseEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_wallpaperCloseExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation; -import static com.android.internal.R.styleable.WindowAnimation_wallpaperOpenEnterAnimation; -import static com.android.internal.R.styleable.WindowAnimation_wallpaperOpenExitAnimation; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS_ANIM; -import static com.android.server.wm.AppTransitionProto.APP_TRANSITION_STATE; -import static com.android.server.wm.AppTransitionProto.LAST_USED_APP_TRANSITION; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; -import static com.android.server.wm.WindowManagerInternal.AppTransitionListener; -import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_AFTER_ANIM; -import static com.android.server.wm.WindowStateAnimator.ROOT_TASK_CLIP_NONE; - -import android.annotation.ColorInt; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.hardware.HardwareBuffer; -import android.os.Binder; -import android.os.Debug; -import android.os.Handler; -import android.os.IBinder; -import android.os.IRemoteCallback; -import android.os.RemoteException; -import android.os.SystemClock; -import android.os.UserHandle; -import android.util.Pair; -import android.util.Slog; -import android.util.SparseArray; -import android.util.proto.ProtoOutputStream; -import android.view.AppTransitionAnimationSpec; -import android.view.IAppTransitionAnimationSpecsFuture; -import android.view.RemoteAnimationAdapter; -import android.view.WindowManager.TransitionFlags; -import android.view.WindowManager.TransitionOldType; -import android.view.WindowManager.TransitionType; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; -import android.view.animation.ScaleAnimation; -import android.view.animation.TranslateAnimation; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.TransitionAnimation; -import com.android.internal.protolog.ProtoLog; -import com.android.internal.protolog.common.LogLevel; -import com.android.internal.util.DumpUtils.Dump; -import com.android.internal.util.function.pooled.PooledLambda; -import com.android.internal.util.function.pooled.PooledPredicate; -import com.android.server.wm.ActivityRecord.CustomAppTransition; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -// State management of app transitions. When we are preparing for a -// transition, mNextAppTransition will be the kind of transition to -// perform or TRANSIT_NONE if we are not waiting. If we are waiting, -// mOpeningApps and mClosingApps are the lists of tokens that will be -// made visible or hidden at the next transition. -public class AppTransition implements Dump { - private static final String TAG = TAG_WITH_CLASS_NAME ? "AppTransition" : TAG_WM; - - static final int DEFAULT_APP_TRANSITION_DURATION = 336; - - /** - * Maximum duration for the clip reveal animation. This is used when there is a lot of movement - * involved, to make it more understandable. - */ - private static final long APP_TRANSITION_TIMEOUT_MS = 5000; - static final int MAX_APP_TRANSITION_DURATION = 3 * 1000; // 3 secs. - - private final Context mContext; - private final WindowManagerService mService; - private final DisplayContent mDisplayContent; - - @VisibleForTesting - final TransitionAnimation mTransitionAnimation; - - private @TransitionFlags int mNextAppTransitionFlags = 0; - private final ArrayList<Integer> mNextAppTransitionRequests = new ArrayList<>(); - private @TransitionOldType int mLastUsedAppTransition = TRANSIT_OLD_UNSET; - private String mLastOpeningApp; - private String mLastClosingApp; - private String mLastChangingApp; - - private static final int NEXT_TRANSIT_TYPE_NONE = 0; - private static final int NEXT_TRANSIT_TYPE_CUSTOM = 1; - private static final int NEXT_TRANSIT_TYPE_SCALE_UP = 2; - private static final int NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP = 3; - private static final int NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN = 4; - private static final int NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP = 5; - private static final int NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN = 6; - private static final int NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE = 7; - private static final int NEXT_TRANSIT_TYPE_CLIP_REVEAL = 8; - - /** - * Refers to the transition to activity started by using {@link - * android.content.pm.crossprofile.CrossProfileApps#startMainActivity(ComponentName, UserHandle) - * }. - */ - private static final int NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS = 9; - private static final int NEXT_TRANSIT_TYPE_REMOTE = 10; - - private int mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE; - private boolean mNextAppTransitionOverrideRequested; - - private String mNextAppTransitionPackage; - // Used for thumbnail transitions. True if we're scaling up, false if scaling down - private boolean mNextAppTransitionScaleUp; - private IRemoteCallback mNextAppTransitionCallback; - private IRemoteCallback mNextAppTransitionFutureCallback; - private IRemoteCallback mAnimationFinishedCallback; - private int mNextAppTransitionEnter; - private int mNextAppTransitionExit; - private @ColorInt int mNextAppTransitionBackgroundColor; - private int mNextAppTransitionInPlace; - private boolean mNextAppTransitionIsSync; - - // Keyed by WindowContainer hashCode. - private final SparseArray<AppTransitionAnimationSpec> mNextAppTransitionAnimationsSpecs - = new SparseArray<>(); - private IAppTransitionAnimationSpecsFuture mNextAppTransitionAnimationsSpecsFuture; - private boolean mNextAppTransitionAnimationsSpecsPending; - private AppTransitionAnimationSpec mDefaultNextAppTransitionAnimationSpec; - - private final Rect mTmpRect = new Rect(); - - private final static int APP_STATE_IDLE = 0; - private final static int APP_STATE_READY = 1; - private final static int APP_STATE_RUNNING = 2; - private final static int APP_STATE_TIMEOUT = 3; - private int mAppTransitionState = APP_STATE_IDLE; - - private final ArrayList<AppTransitionListener> mListeners = new ArrayList<>(); - private final ExecutorService mDefaultExecutor = Executors.newSingleThreadExecutor(); - - private final int mDefaultWindowAnimationStyleResId; - private boolean mOverrideTaskTransition; - - final Handler mHandler; - final Runnable mHandleAppTransitionTimeoutRunnable = () -> handleAppTransitionTimeout(); - - AppTransition(Context context, WindowManagerService service, DisplayContent displayContent) { - mContext = context; - mService = service; - mHandler = new Handler(service.mH.getLooper()); - mDisplayContent = displayContent; - mTransitionAnimation = new TransitionAnimation( - context, ProtoLog.isEnabled(WM_DEBUG_ANIM, LogLevel.DEBUG), TAG); - - final TypedArray windowStyle = mContext.getTheme().obtainStyledAttributes( - com.android.internal.R.styleable.Window); - mDefaultWindowAnimationStyleResId = windowStyle.getResourceId( - com.android.internal.R.styleable.Window_windowAnimationStyle, 0); - windowStyle.recycle(); - } - - boolean isTransitionSet() { - return !mNextAppTransitionRequests.isEmpty(); - } - - boolean isUnoccluding() { - return mNextAppTransitionRequests.contains(TRANSIT_KEYGUARD_UNOCCLUDE); - } - - boolean transferFrom(AppTransition other) { - mNextAppTransitionRequests.addAll(other.mNextAppTransitionRequests); - return prepare(); - } - - void setLastAppTransition(@TransitionOldType int transit, ActivityRecord openingApp, - ActivityRecord closingApp, ActivityRecord changingApp) { - mLastUsedAppTransition = transit; - mLastOpeningApp = "" + openingApp; - mLastClosingApp = "" + closingApp; - mLastChangingApp = "" + changingApp; - } - - boolean isReady() { - return mAppTransitionState == APP_STATE_READY - || mAppTransitionState == APP_STATE_TIMEOUT; - } - - void setReady() { - setAppTransitionState(APP_STATE_READY); - fetchAppTransitionSpecsFromFuture(); - } - - boolean isRunning() { - return mAppTransitionState == APP_STATE_RUNNING; - } - - void setIdle() { - setAppTransitionState(APP_STATE_IDLE); - } - - boolean isIdle() { - return mAppTransitionState == APP_STATE_IDLE; - } - - boolean isTimeout() { - return mAppTransitionState == APP_STATE_TIMEOUT; - } - - void setTimeout() { - setAppTransitionState(APP_STATE_TIMEOUT); - } - - /** - * Gets the animation overridden by app via {@link #overridePendingAppTransition}. - */ - @Nullable - Animation getNextAppRequestedAnimation(boolean enter) { - final Animation a = mTransitionAnimation.loadAppTransitionAnimation( - mNextAppTransitionPackage, - enter ? mNextAppTransitionEnter : mNextAppTransitionExit); - if (mNextAppTransitionBackgroundColor != 0 && a != null) { - a.setBackdropColor(mNextAppTransitionBackgroundColor); - } - return a; - } - - /** - * Gets the animation background color overridden by app via - * {@link #overridePendingAppTransition}. - */ - @ColorInt int getNextAppTransitionBackgroundColor() { - return mNextAppTransitionBackgroundColor; - } - - @VisibleForTesting - boolean isNextAppTransitionOverrideRequested() { - return mNextAppTransitionOverrideRequested; - } - - HardwareBuffer getAppTransitionThumbnailHeader(WindowContainer container) { - AppTransitionAnimationSpec spec = mNextAppTransitionAnimationsSpecs.get( - container.hashCode()); - if (spec == null) { - spec = mDefaultNextAppTransitionAnimationSpec; - } - return spec != null ? spec.buffer : null; - } - - /** Returns whether the next thumbnail transition is aspect scaled up. */ - boolean isNextThumbnailTransitionAspectScaled() { - return mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP || - mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN; - } - - /** Returns whether the next thumbnail transition is scaling up. */ - boolean isNextThumbnailTransitionScaleUp() { - return mNextAppTransitionScaleUp; - } - - boolean isNextAppTransitionThumbnailUp() { - return mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP || - mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP; - } - - boolean isNextAppTransitionThumbnailDown() { - return mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN || - mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN; - } - - boolean isNextAppTransitionOpenCrossProfileApps() { - return mNextAppTransitionType == NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS; - } - - /** - * @return true if and only if we are currently fetching app transition specs from the future - * passed into {@link #overridePendingAppTransitionMultiThumbFuture} - */ - boolean isFetchingAppTransitionsSpecs() { - return mNextAppTransitionAnimationsSpecsPending; - } - - private boolean prepare() { - if (!isRunning()) { - setAppTransitionState(APP_STATE_IDLE); - notifyAppTransitionPendingLocked(); - return true; - } - return false; - } - - /** - * @return bit-map of WindowManagerPolicy#FINISH_LAYOUT_REDO_* to indicate whether another - * layout pass needs to be done - */ - int goodToGo(@TransitionOldType int transit, ActivityRecord topOpeningApp) { - mNextAppTransitionFlags = 0; - mNextAppTransitionRequests.clear(); - setAppTransitionState(APP_STATE_RUNNING); - final WindowContainer wc = - topOpeningApp != null ? topOpeningApp.getAnimatingContainer() : null; - final AnimationAdapter topOpeningAnim = wc != null ? wc.getAnimation() : null; - - int redoLayout = notifyAppTransitionStartingLocked( - topOpeningAnim != null - ? topOpeningAnim.getStatusBarTransitionsStartTime() - : SystemClock.uptimeMillis(), - AnimationAdapter.STATUS_BAR_TRANSITION_DURATION); - - if ((isTaskOpenTransitOld(transit) || transit == TRANSIT_OLD_WALLPAPER_CLOSE) - && topOpeningAnim != null) { - if (mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()) { - final NavBarFadeAnimationController controller = - new NavBarFadeAnimationController(mDisplayContent); - // For remote animation case, the nav bar fades out and in is controlled by the - // remote side. For non-remote animation case, we play the fade out/in animation - // here. We play the nav bar fade-out animation when the app transition animation - // starts and play the fade-in animation sequentially once the fade-out is finished. - controller.fadeOutAndInSequentially(topOpeningAnim.getDurationHint(), - null /* fadeOutParent */, topOpeningApp.getSurfaceControl()); - } - } - return redoLayout; - } - - void clear() { - clear(true /* clearAppOverride */); - } - - private void clear(boolean clearAppOverride) { - mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE; - mNextAppTransitionOverrideRequested = false; - mNextAppTransitionAnimationsSpecs.clear(); - mNextAppTransitionAnimationsSpecsFuture = null; - mDefaultNextAppTransitionAnimationSpec = null; - mAnimationFinishedCallback = null; - mOverrideTaskTransition = false; - mNextAppTransitionIsSync = false; - if (clearAppOverride) { - mNextAppTransitionPackage = null; - mNextAppTransitionEnter = 0; - mNextAppTransitionExit = 0; - mNextAppTransitionBackgroundColor = 0; - } - } - - void freeze() { - final boolean keyguardGoingAwayCancelled = mNextAppTransitionRequests.contains( - TRANSIT_KEYGUARD_GOING_AWAY); - - mNextAppTransitionRequests.clear(); - clear(); - setReady(); - notifyAppTransitionCancelledLocked(keyguardGoingAwayCancelled); - } - - private void setAppTransitionState(int state) { - mAppTransitionState = state; - updateBooster(); - } - - /** - * Updates whether we currently boost wm locked sections and the animation thread. We want to - * boost the priorities to a more important value whenever an app transition is going to happen - * soon or an app transition is running. - */ - void updateBooster() { - WindowManagerService.sThreadPriorityBooster.setAppTransitionRunning(needsBoosting()); - } - - private boolean needsBoosting() { - return !mNextAppTransitionRequests.isEmpty() - || mAppTransitionState == APP_STATE_READY - || mAppTransitionState == APP_STATE_RUNNING; - } - - void registerListenerLocked(AppTransitionListener listener) { - mListeners.add(listener); - } - - void unregisterListener(AppTransitionListener listener) { - mListeners.remove(listener); - } - - public void notifyAppTransitionFinishedLocked(IBinder token) { - for (int i = 0; i < mListeners.size(); i++) { - mListeners.get(i).onAppTransitionFinishedLocked(token); - } - } - - private void notifyAppTransitionPendingLocked() { - for (int i = 0; i < mListeners.size(); i++) { - mListeners.get(i).onAppTransitionPendingLocked(); - } - } - - private void notifyAppTransitionCancelledLocked(boolean keyguardGoingAwayCancelled) { - for (int i = 0; i < mListeners.size(); i++) { - mListeners.get(i).onAppTransitionCancelledLocked(keyguardGoingAwayCancelled); - } - } - - private void notifyAppTransitionTimeoutLocked() { - for (int i = 0; i < mListeners.size(); i++) { - mListeners.get(i).onAppTransitionTimeoutLocked(); - } - } - - private int notifyAppTransitionStartingLocked(long statusBarAnimationStartTime, - long statusBarAnimationDuration) { - int redoLayout = 0; - for (int i = 0; i < mListeners.size(); i++) { - redoLayout |= mListeners.get(i).onAppTransitionStartingLocked( - statusBarAnimationStartTime, statusBarAnimationDuration); - } - return redoLayout; - } - - @VisibleForTesting - int getDefaultWindowAnimationStyleResId() { - return mDefaultWindowAnimationStyleResId; - } - - /** Returns window animation style ID from {@link LayoutParams} or from system in some cases */ - @VisibleForTesting - int getAnimationStyleResId(@NonNull LayoutParams lp) { - return mTransitionAnimation.getAnimationStyleResId(lp); - } - - @VisibleForTesting - @Nullable - Animation loadAnimationSafely(Context context, int resId) { - return TransitionAnimation.loadAnimationSafely(context, resId, TAG); - } - - private static int mapOpenCloseTransitTypes(int transit, boolean enter) { - int animAttr = 0; - switch (transit) { - case TRANSIT_OLD_ACTIVITY_OPEN: - case TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN: - animAttr = enter - ? WindowAnimation_activityOpenEnterAnimation - : WindowAnimation_activityOpenExitAnimation; - break; - case TRANSIT_OLD_ACTIVITY_CLOSE: - case TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE: - animAttr = enter - ? WindowAnimation_activityCloseEnterAnimation - : WindowAnimation_activityCloseExitAnimation; - break; - case TRANSIT_OLD_TASK_OPEN: - animAttr = enter - ? WindowAnimation_taskOpenEnterAnimation - : WindowAnimation_taskOpenExitAnimation; - break; - case TRANSIT_OLD_TASK_CLOSE: - animAttr = enter - ? WindowAnimation_taskCloseEnterAnimation - : WindowAnimation_taskCloseExitAnimation; - break; - case TRANSIT_OLD_TASK_TO_FRONT: - animAttr = enter - ? WindowAnimation_taskToFrontEnterAnimation - : WindowAnimation_taskToFrontExitAnimation; - break; - case TRANSIT_OLD_TASK_TO_BACK: - animAttr = enter - ? WindowAnimation_taskToBackEnterAnimation - : WindowAnimation_taskToBackExitAnimation; - break; - case TRANSIT_OLD_WALLPAPER_OPEN: - animAttr = enter - ? WindowAnimation_wallpaperOpenEnterAnimation - : WindowAnimation_wallpaperOpenExitAnimation; - break; - case TRANSIT_OLD_WALLPAPER_CLOSE: - animAttr = enter - ? WindowAnimation_wallpaperCloseEnterAnimation - : WindowAnimation_wallpaperCloseExitAnimation; - break; - case TRANSIT_OLD_WALLPAPER_INTRA_OPEN: - animAttr = enter - ? WindowAnimation_wallpaperIntraOpenEnterAnimation - : WindowAnimation_wallpaperIntraOpenExitAnimation; - break; - case TRANSIT_OLD_WALLPAPER_INTRA_CLOSE: - animAttr = enter - ? WindowAnimation_wallpaperIntraCloseEnterAnimation - : WindowAnimation_wallpaperIntraCloseExitAnimation; - break; - case TRANSIT_OLD_TASK_OPEN_BEHIND: - animAttr = enter - ? WindowAnimation_launchTaskBehindSourceAnimation - : WindowAnimation_launchTaskBehindTargetAnimation; - break; - // TODO(b/189386466): Use activity transition as the fallback. Investigate if we - // need new TaskFragment transition. - case TRANSIT_OLD_TASK_FRAGMENT_OPEN: - animAttr = enter - ? WindowAnimation_activityOpenEnterAnimation - : WindowAnimation_activityOpenExitAnimation; - break; - // TODO(b/189386466): Use activity transition as the fallback. Investigate if we - // need new TaskFragment transition. - case TRANSIT_OLD_TASK_FRAGMENT_CLOSE: - animAttr = enter - ? WindowAnimation_activityCloseEnterAnimation - : WindowAnimation_activityCloseExitAnimation; - break; - case TRANSIT_OLD_DREAM_ACTIVITY_OPEN: - animAttr = enter - ? WindowAnimation_dreamActivityOpenEnterAnimation - : WindowAnimation_dreamActivityOpenExitAnimation; - break; - case TRANSIT_OLD_DREAM_ACTIVITY_CLOSE: - animAttr = enter - ? 0 - : WindowAnimation_dreamActivityCloseExitAnimation; - break; - } - - return animAttr; - } - - @Nullable - Animation loadAnimationAttr(LayoutParams lp, int animAttr, int transit) { - return mTransitionAnimation.loadAnimationAttr(lp, animAttr, transit); - } - - private void getDefaultNextAppTransitionStartRect(Rect rect) { - if (mDefaultNextAppTransitionAnimationSpec == null || - mDefaultNextAppTransitionAnimationSpec.rect == null) { - Slog.e(TAG, "Starting rect for app requested, but none available", new Throwable()); - rect.setEmpty(); - } else { - rect.set(mDefaultNextAppTransitionAnimationSpec.rect); - } - } - - private void putDefaultNextAppTransitionCoordinates(int left, int top, int width, int height, - HardwareBuffer buffer) { - mDefaultNextAppTransitionAnimationSpec = new AppTransitionAnimationSpec(-1 /* taskId */, - buffer, new Rect(left, top, left + width, top + height)); - } - - /** - * Creates an overlay with a background color and a thumbnail for the cross profile apps - * animation. - */ - HardwareBuffer createCrossProfileAppsThumbnail( - Drawable thumbnailDrawable, Rect frame) { - return mTransitionAnimation.createCrossProfileAppsThumbnail(thumbnailDrawable, frame); - } - - Animation createCrossProfileAppsThumbnailAnimationLocked(Rect appRect) { - return mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(appRect); - } - - /** - * This animation runs for the thumbnail that gets cross faded with the enter/exit activity - * when a thumbnail is specified with the pending animation override. - */ - Animation createThumbnailAspectScaleAnimationLocked(Rect appRect, @Nullable Rect contentInsets, - HardwareBuffer thumbnailHeader, WindowContainer container, int orientation) { - AppTransitionAnimationSpec spec = mNextAppTransitionAnimationsSpecs.get( - container.hashCode()); - return mTransitionAnimation.createThumbnailAspectScaleAnimationLocked(appRect, - contentInsets, thumbnailHeader, orientation, spec != null ? spec.rect : null, - mDefaultNextAppTransitionAnimationSpec != null - ? mDefaultNextAppTransitionAnimationSpec.rect : null, - mNextAppTransitionScaleUp); - } - - private AnimationSet createAspectScaledThumbnailFreeformAnimationLocked(Rect sourceFrame, - Rect destFrame, @Nullable Rect surfaceInsets, boolean enter) { - final float sourceWidth = sourceFrame.width(); - final float sourceHeight = sourceFrame.height(); - final float destWidth = destFrame.width(); - final float destHeight = destFrame.height(); - final float scaleH = enter ? sourceWidth / destWidth : destWidth / sourceWidth; - final float scaleV = enter ? sourceHeight / destHeight : destHeight / sourceHeight; - AnimationSet set = new AnimationSet(true); - final int surfaceInsetsH = surfaceInsets == null - ? 0 : surfaceInsets.left + surfaceInsets.right; - final int surfaceInsetsV = surfaceInsets == null - ? 0 : surfaceInsets.top + surfaceInsets.bottom; - // We want the scaling to happen from the center of the surface. In order to achieve that, - // we need to account for surface insets that will be used to enlarge the surface. - final float scaleHCenter = ((enter ? destWidth : sourceWidth) + surfaceInsetsH) / 2; - final float scaleVCenter = ((enter ? destHeight : sourceHeight) + surfaceInsetsV) / 2; - final ScaleAnimation scale = enter ? - new ScaleAnimation(scaleH, 1, scaleV, 1, scaleHCenter, scaleVCenter) - : new ScaleAnimation(1, scaleH, 1, scaleV, scaleHCenter, scaleVCenter); - final int sourceHCenter = sourceFrame.left + sourceFrame.width() / 2; - final int sourceVCenter = sourceFrame.top + sourceFrame.height() / 2; - final int destHCenter = destFrame.left + destFrame.width() / 2; - final int destVCenter = destFrame.top + destFrame.height() / 2; - final int fromX = enter ? sourceHCenter - destHCenter : destHCenter - sourceHCenter; - final int fromY = enter ? sourceVCenter - destVCenter : destVCenter - sourceVCenter; - final TranslateAnimation translation = enter ? new TranslateAnimation(fromX, 0, fromY, 0) - : new TranslateAnimation(0, fromX, 0, fromY); - set.addAnimation(scale); - set.addAnimation(translation); - setAppTransitionFinishedCallbackIfNeeded(set); - return set; - } - - /** - * @return true if and only if the first frame of the transition can be skipped, i.e. the first - * frame of the transition doesn't change the visuals on screen, so we can start - * directly with the second one - */ - boolean canSkipFirstFrame() { - return mNextAppTransitionType != NEXT_TRANSIT_TYPE_CUSTOM - && !mNextAppTransitionOverrideRequested - && mNextAppTransitionType != NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE - && mNextAppTransitionType != NEXT_TRANSIT_TYPE_CLIP_REVEAL - && !mNextAppTransitionRequests.contains(TRANSIT_KEYGUARD_GOING_AWAY); - } - - /** - * - * @param frame These are the bounds of the window when it finishes the animation. This is where - * the animation must usually finish in entrance animation, as the next frame will - * display the window at these coordinates. In case of exit animation, this is - * where the animation must start, as the frame before the animation is displaying - * the window at these bounds. - * @param insets Knowing where the window will be positioned is not enough. Some parts of the - * window might be obscured, usually by the system windows (status bar and - * navigation bar) and we use content insets to convey that information. This - * usually affects the animation aspects vertically, as the system decoration is - * at the top and the bottom. For example when we animate from full screen to - * recents, we want to exclude the covered parts, because they won't match the - * thumbnail after the last frame is executed. - * @param surfaceInsets In rare situation the surface is larger than the content and we need to - * know about this to make the animation frames match. We currently use - * this for freeform windows, which have larger surfaces to display - * shadows. When we animate them from recents, we want to match the content - * to the recents thumbnail and hence need to account for the surface being - * bigger. - */ - @Nullable - Animation loadAnimation(LayoutParams lp, int transit, boolean enter, int uiMode, - int orientation, Rect frame, Rect displayFrame, Rect insets, - @Nullable Rect surfaceInsets, @Nullable Rect stableInsets, boolean isVoiceInteraction, - boolean freeform, WindowContainer container) { - - final boolean canCustomizeAppTransition = container.canCustomizeAppTransition(); - - if (mNextAppTransitionOverrideRequested) { - if (canCustomizeAppTransition || mOverrideTaskTransition) { - mNextAppTransitionType = NEXT_TRANSIT_TYPE_CUSTOM; - } else { - ProtoLog.e(WM_DEBUG_APP_TRANSITIONS_ANIM, "applyAnimation: " - + " override requested, but it is prohibited by policy."); - } - } - - Animation a; - if (isKeyguardGoingAwayTransitOld(transit) && enter) { - a = mTransitionAnimation.loadKeyguardExitAnimation(mNextAppTransitionFlags, - transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER); - } else if (transit == TRANSIT_OLD_KEYGUARD_OCCLUDE - || transit == TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM) { - a = null; - } else if (transit == TRANSIT_OLD_KEYGUARD_UNOCCLUDE && !enter) { - a = mTransitionAnimation.loadKeyguardUnoccludeAnimation(); - } else if (transit == TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE) { - a = null; - } else if (isVoiceInteraction && (transit == TRANSIT_OLD_ACTIVITY_OPEN - || transit == TRANSIT_OLD_TASK_OPEN - || transit == TRANSIT_OLD_TASK_TO_FRONT)) { - a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation voice: anim=%s transit=%s isEntrance=%b Callers=%s", a, - appTransitionOldToString(transit), enter, Debug.getCallers(3)); - } else if (isVoiceInteraction && (transit == TRANSIT_OLD_ACTIVITY_CLOSE - || transit == TRANSIT_OLD_TASK_CLOSE - || transit == TRANSIT_OLD_TASK_TO_BACK)) { - a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation voice: anim=%s transit=%s isEntrance=%b Callers=%s", a, - appTransitionOldToString(transit), enter, Debug.getCallers(3)); - } else if (transit == TRANSIT_OLD_ACTIVITY_RELAUNCH) { - a = mTransitionAnimation.createRelaunchAnimation(frame, insets, - mDefaultNextAppTransitionAnimationSpec != null - ? mDefaultNextAppTransitionAnimationSpec.rect : null); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s transit=%s Callers=%s", a, - appTransitionOldToString(transit), Debug.getCallers(3)); - } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CUSTOM) { - a = getNextAppRequestedAnimation(enter); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s nextAppTransition=ANIM_CUSTOM transit=%s " - + "isEntrance=%b Callers=%s", - a, appTransitionOldToString(transit), enter, Debug.getCallers(3)); - } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE) { - a = mTransitionAnimation.loadAppTransitionAnimation( - mNextAppTransitionPackage, mNextAppTransitionInPlace); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s nextAppTransition=ANIM_CUSTOM_IN_PLACE " - + "transit=%s Callers=%s", - a, appTransitionOldToString(transit), Debug.getCallers(3)); - } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CLIP_REVEAL) { - a = mTransitionAnimation.createClipRevealAnimationLockedCompat( - transit, enter, frame, displayFrame, - mDefaultNextAppTransitionAnimationSpec != null - ? mDefaultNextAppTransitionAnimationSpec.rect : null); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s nextAppTransition=ANIM_CLIP_REVEAL " - + "transit=%s Callers=%s", - a, appTransitionOldToString(transit), Debug.getCallers(3)); - } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_SCALE_UP) { - a = mTransitionAnimation.createScaleUpAnimationLockedCompat(transit, enter, frame, - mDefaultNextAppTransitionAnimationSpec != null - ? mDefaultNextAppTransitionAnimationSpec.rect : null); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s nextAppTransition=ANIM_SCALE_UP transit=%s " - + "isEntrance=%s Callers=%s", - a, appTransitionOldToString(transit), enter, Debug.getCallers(3)); - } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP || - mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN) { - mNextAppTransitionScaleUp = - (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP); - final HardwareBuffer thumbnailHeader = getAppTransitionThumbnailHeader(container); - a = mTransitionAnimation.createThumbnailEnterExitAnimationLockedCompat(enter, - mNextAppTransitionScaleUp, frame, transit, thumbnailHeader, - mDefaultNextAppTransitionAnimationSpec != null - ? mDefaultNextAppTransitionAnimationSpec.rect : null); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s nextAppTransition=%s transit=%s isEntrance=%b " - + "Callers=%s", - a, mNextAppTransitionScaleUp - ? "ANIM_THUMBNAIL_SCALE_UP" : "ANIM_THUMBNAIL_SCALE_DOWN", - appTransitionOldToString(transit), enter, Debug.getCallers(3)); - } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP || - mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN) { - mNextAppTransitionScaleUp = - (mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP); - AppTransitionAnimationSpec spec = mNextAppTransitionAnimationsSpecs.get( - container.hashCode()); - a = mTransitionAnimation.createAspectScaledThumbnailEnterExitAnimationLocked(enter, - mNextAppTransitionScaleUp, orientation, transit, frame, insets, surfaceInsets, - stableInsets, freeform, spec != null ? spec.rect : null, - mDefaultNextAppTransitionAnimationSpec != null - ? mDefaultNextAppTransitionAnimationSpec.rect : null); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s nextAppTransition=%s transit=%s isEntrance=%b " - + "Callers=%s", - a, mNextAppTransitionScaleUp - ? "ANIM_THUMBNAIL_ASPECT_SCALE_UP" - : "ANIM_THUMBNAIL_ASPECT_SCALE_DOWN", - appTransitionOldToString(transit), enter, Debug.getCallers(3)); - } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS && enter) { - a = mTransitionAnimation.loadCrossProfileAppEnterAnimation(); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS: " - + "anim=%s transit=%s isEntrance=true Callers=%s", - a, appTransitionOldToString(transit), Debug.getCallers(3)); - } else if (isChangeTransitOld(transit)) { - // In the absence of a specific adapter, we just want to keep everything stationary. - a = new AlphaAnimation(1.f, 1.f); - a.setDuration(WindowChangeAnimationSpec.ANIMATION_DURATION); - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s transit=%s isEntrance=%b Callers=%s", - a, appTransitionOldToString(transit), enter, Debug.getCallers(3)); - } else { - int animAttr = mapOpenCloseTransitTypes(transit, enter); - if (animAttr != 0) { - final CustomAppTransition customAppTransition = - getCustomAppTransition(animAttr, container); - if (customAppTransition != null) { - a = loadCustomActivityAnimation(customAppTransition, enter, container); - } else { - if (canCustomizeAppTransition) { - a = loadAnimationAttr(lp, animAttr, transit); - } else { - a = mTransitionAnimation.loadDefaultAnimationAttr(animAttr, transit); - } - } - } else { - a = null; - } - - ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, - "applyAnimation: anim=%s animAttr=0x%x transit=%s isEntrance=%b " - + " canCustomizeAppTransition=%b Callers=%s", - a, animAttr, appTransitionOldToString(transit), enter, - canCustomizeAppTransition, Debug.getCallers(3)); - } - setAppTransitionFinishedCallbackIfNeeded(a); - - return a; - } - - CustomAppTransition getCustomAppTransition(int animAttr, WindowContainer container) { - ActivityRecord customAnimationSource = container.asActivityRecord(); - if (customAnimationSource == null) { - return null; - } - - // Only top activity can customize activity animation. - // If the animation is for the one below, try to get from the above activity. - if (animAttr == WindowAnimation_activityOpenExitAnimation - || animAttr == WindowAnimation_activityCloseEnterAnimation) { - customAnimationSource = customAnimationSource.getTask() - .getActivityAbove(customAnimationSource); - if (customAnimationSource == null) { - return null; - } - } - switch (animAttr) { - case WindowAnimation_activityOpenEnterAnimation: - case WindowAnimation_activityOpenExitAnimation: - return customAnimationSource.getCustomAnimation(true /* open */); - case WindowAnimation_activityCloseEnterAnimation: - case WindowAnimation_activityCloseExitAnimation: - return customAnimationSource.getCustomAnimation(false /* open */); - } - return null; - } - private Animation loadCustomActivityAnimation(@NonNull CustomAppTransition custom, - boolean enter, WindowContainer container) { - final ActivityRecord customAnimationSource = container.asActivityRecord(); - final Animation a = mTransitionAnimation.loadAppTransitionAnimation( - customAnimationSource.packageName, enter - ? custom.mEnterAnim : custom.mExitAnim); - if (a != null && custom.mBackgroundColor != 0) { - a.setBackdropColor(custom.mBackgroundColor); - a.setShowBackdrop(true); - } - return a; - } - - int getAppRootTaskClipMode() { - return mNextAppTransitionRequests.contains(TRANSIT_RELAUNCH) - || mNextAppTransitionRequests.contains(TRANSIT_KEYGUARD_GOING_AWAY) - || mNextAppTransitionType == NEXT_TRANSIT_TYPE_CLIP_REVEAL - ? ROOT_TASK_CLIP_NONE - : ROOT_TASK_CLIP_AFTER_ANIM; - } - - @TransitionFlags - public int getTransitFlags() { - return mNextAppTransitionFlags; - } - - void postAnimationCallback() { - if (mNextAppTransitionCallback != null) { - mHandler.sendMessage(PooledLambda.obtainMessage(AppTransition::doAnimationCallback, - mNextAppTransitionCallback)); - mNextAppTransitionCallback = null; - } - } - - void overridePendingAppTransition(String packageName, int enterAnim, int exitAnim, - @ColorInt int backgroundColor, IRemoteCallback startedCallback, - IRemoteCallback endedCallback, boolean overrideTaskTransaction) { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionOverrideRequested = true; - mNextAppTransitionPackage = packageName; - mNextAppTransitionEnter = enterAnim; - mNextAppTransitionExit = exitAnim; - mNextAppTransitionBackgroundColor = backgroundColor; - postAnimationCallback(); - mNextAppTransitionCallback = startedCallback; - mAnimationFinishedCallback = endedCallback; - mOverrideTaskTransition = overrideTaskTransaction; - } - } - - void overridePendingAppTransitionScaleUp(int startX, int startY, int startWidth, - int startHeight) { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionType = NEXT_TRANSIT_TYPE_SCALE_UP; - putDefaultNextAppTransitionCoordinates(startX, startY, startWidth, startHeight, null); - postAnimationCallback(); - } - } - - void overridePendingAppTransitionClipReveal(int startX, int startY, - int startWidth, int startHeight) { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionType = NEXT_TRANSIT_TYPE_CLIP_REVEAL; - putDefaultNextAppTransitionCoordinates(startX, startY, startWidth, startHeight, null); - postAnimationCallback(); - } - } - - void overridePendingAppTransitionThumb(HardwareBuffer srcThumb, int startX, int startY, - IRemoteCallback startedCallback, boolean scaleUp) { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP - : NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN; - mNextAppTransitionScaleUp = scaleUp; - putDefaultNextAppTransitionCoordinates(startX, startY, 0, 0, srcThumb); - postAnimationCallback(); - mNextAppTransitionCallback = startedCallback; - } - } - - void overridePendingAppTransitionAspectScaledThumb(HardwareBuffer srcThumb, int startX, - int startY, int targetWidth, int targetHeight, IRemoteCallback startedCallback, - boolean scaleUp) { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP - : NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN; - mNextAppTransitionScaleUp = scaleUp; - putDefaultNextAppTransitionCoordinates(startX, startY, targetWidth, targetHeight, - srcThumb); - postAnimationCallback(); - mNextAppTransitionCallback = startedCallback; - } - } - - void overridePendingAppTransitionMultiThumb(AppTransitionAnimationSpec[] specs, - IRemoteCallback onAnimationStartedCallback, IRemoteCallback onAnimationFinishedCallback, - boolean scaleUp) { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP - : NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN; - mNextAppTransitionScaleUp = scaleUp; - if (specs != null) { - for (int i = 0; i < specs.length; i++) { - AppTransitionAnimationSpec spec = specs[i]; - if (spec != null) { - final PooledPredicate p = PooledLambda.obtainPredicate( - Task::isTaskId, PooledLambda.__(Task.class), spec.taskId); - final WindowContainer container = mDisplayContent.getTask(p); - p.recycle(); - if (container == null) { - continue; - } - mNextAppTransitionAnimationsSpecs.put(container.hashCode(), spec); - if (i == 0) { - // In full screen mode, the transition code depends on the default spec - // to be set. - Rect rect = spec.rect; - putDefaultNextAppTransitionCoordinates(rect.left, rect.top, - rect.width(), rect.height(), spec.buffer); - } - } - } - } - postAnimationCallback(); - mNextAppTransitionCallback = onAnimationStartedCallback; - mAnimationFinishedCallback = onAnimationFinishedCallback; - } - } - - void overridePendingAppTransitionMultiThumbFuture( - IAppTransitionAnimationSpecsFuture specsFuture, IRemoteCallback callback, - boolean scaleUp) { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP - : NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN; - mNextAppTransitionAnimationsSpecsFuture = specsFuture; - mNextAppTransitionScaleUp = scaleUp; - mNextAppTransitionFutureCallback = callback; - if (isReady()) { - fetchAppTransitionSpecsFromFuture(); - } - } - } - - void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter) { - overridePendingAppTransitionRemote(remoteAnimationAdapter, false /* sync */, - false /* isActivityEmbedding*/); - } - - void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter, - boolean sync, boolean isActivityEmbedding) { - } - - void overrideInPlaceAppTransition(String packageName, int anim) { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionType = NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE; - mNextAppTransitionPackage = packageName; - mNextAppTransitionInPlace = anim; - } - } - - /** - * @see {@link #NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS} - */ - void overridePendingAppTransitionStartCrossProfileApps() { - if (canOverridePendingAppTransition()) { - clear(); - mNextAppTransitionType = NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS; - postAnimationCallback(); - } - } - - private boolean canOverridePendingAppTransition() { - // Remote animations always take precedence - return isTransitionSet() && mNextAppTransitionType != NEXT_TRANSIT_TYPE_REMOTE; - } - - /** - * If a future is set for the app transition specs, fetch it in another thread. - */ - private void fetchAppTransitionSpecsFromFuture() { - if (mNextAppTransitionAnimationsSpecsFuture != null) { - mNextAppTransitionAnimationsSpecsPending = true; - final IAppTransitionAnimationSpecsFuture future - = mNextAppTransitionAnimationsSpecsFuture; - mNextAppTransitionAnimationsSpecsFuture = null; - mDefaultExecutor.execute(() -> { - AppTransitionAnimationSpec[] specs = null; - try { - Binder.allowBlocking(future.asBinder()); - specs = future.get(); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to fetch app transition specs: " + e); - } - synchronized (mService.mGlobalLock) { - mNextAppTransitionAnimationsSpecsPending = false; - overridePendingAppTransitionMultiThumb(specs, - mNextAppTransitionFutureCallback, null /* finishedCallback */, - mNextAppTransitionScaleUp); - mNextAppTransitionFutureCallback = null; - mService.requestTraversal(); - } - }); - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("mNextAppTransitionRequests=["); - - boolean separator = false; - for (Integer transit : mNextAppTransitionRequests) { - if (separator) { - sb.append(", "); - } - sb.append(appTransitionToString(transit)); - separator = true; - } - sb.append("]"); - sb.append(", mNextAppTransitionFlags=" - + appTransitionFlagsToString(mNextAppTransitionFlags)); - return sb.toString(); - } - - /** - * Returns the human readable name of a old window transition. - * - * @param transition The old window transition. - * @return The transition symbolic name. - */ - public static String appTransitionOldToString(@TransitionOldType int transition) { - switch (transition) { - case TRANSIT_OLD_UNSET: { - return "TRANSIT_OLD_UNSET"; - } - case TRANSIT_OLD_NONE: { - return "TRANSIT_OLD_NONE"; - } - case TRANSIT_OLD_ACTIVITY_OPEN: { - return "TRANSIT_OLD_ACTIVITY_OPEN"; - } - case TRANSIT_OLD_ACTIVITY_CLOSE: { - return "TRANSIT_OLD_ACTIVITY_CLOSE"; - } - case TRANSIT_OLD_TASK_OPEN: { - return "TRANSIT_OLD_TASK_OPEN"; - } - case TRANSIT_OLD_TASK_CLOSE: { - return "TRANSIT_OLD_TASK_CLOSE"; - } - case TRANSIT_OLD_TASK_TO_FRONT: { - return "TRANSIT_OLD_TASK_TO_FRONT"; - } - case TRANSIT_OLD_TASK_TO_BACK: { - return "TRANSIT_OLD_TASK_TO_BACK"; - } - case TRANSIT_OLD_WALLPAPER_CLOSE: { - return "TRANSIT_OLD_WALLPAPER_CLOSE"; - } - case TRANSIT_OLD_WALLPAPER_OPEN: { - return "TRANSIT_OLD_WALLPAPER_OPEN"; - } - case TRANSIT_OLD_WALLPAPER_INTRA_OPEN: { - return "TRANSIT_OLD_WALLPAPER_INTRA_OPEN"; - } - case TRANSIT_OLD_WALLPAPER_INTRA_CLOSE: { - return "TRANSIT_OLD_WALLPAPER_INTRA_CLOSE"; - } - case TRANSIT_OLD_TASK_OPEN_BEHIND: { - return "TRANSIT_OLD_TASK_OPEN_BEHIND"; - } - case TRANSIT_OLD_ACTIVITY_RELAUNCH: { - return "TRANSIT_OLD_ACTIVITY_RELAUNCH"; - } - case TRANSIT_OLD_KEYGUARD_GOING_AWAY: { - return "TRANSIT_OLD_KEYGUARD_GOING_AWAY"; - } - case TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER: { - return "TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER"; - } - case TRANSIT_OLD_KEYGUARD_OCCLUDE: { - return "TRANSIT_OLD_KEYGUARD_OCCLUDE"; - } - case TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM: { - return "TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM"; - } - case TRANSIT_OLD_KEYGUARD_UNOCCLUDE: { - return "TRANSIT_OLD_KEYGUARD_UNOCCLUDE"; - } - case TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN: { - return "TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN"; - } - case TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE: { - return "TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE"; - } - case TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE: { - return "TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE"; - } - case TRANSIT_OLD_TASK_FRAGMENT_OPEN: { - return "TRANSIT_OLD_TASK_FRAGMENT_OPEN"; - } - case TRANSIT_OLD_TASK_FRAGMENT_CLOSE: { - return "TRANSIT_OLD_TASK_FRAGMENT_CLOSE"; - } - case TRANSIT_OLD_TASK_FRAGMENT_CHANGE: { - return "TRANSIT_OLD_TASK_FRAGMENT_CHANGE"; - } - case TRANSIT_OLD_DREAM_ACTIVITY_OPEN: { - return "TRANSIT_OLD_DREAM_ACTIVITY_OPEN"; - } - case TRANSIT_OLD_DREAM_ACTIVITY_CLOSE: { - return "TRANSIT_OLD_DREAM_ACTIVITY_CLOSE"; - } - default: { - return "<UNKNOWN: " + transition + ">"; - } - } - } - - /** - * Returns the human readable name of a window transition. - * - * @param transition The window transition. - * @return The transition symbolic name. - */ - public static String appTransitionToString(@TransitionType int transition) { - switch (transition) { - case TRANSIT_NONE: { - return "TRANSIT_NONE"; - } - case TRANSIT_OPEN: { - return "TRANSIT_OPEN"; - } - case TRANSIT_CLOSE: { - return "TRANSIT_CLOSE"; - } - case TRANSIT_TO_FRONT: { - return "TRANSIT_TO_FRONT"; - } - case TRANSIT_TO_BACK: { - return "TRANSIT_TO_BACK"; - } - case TRANSIT_RELAUNCH: { - return "TRANSIT_RELAUNCH"; - } - case TRANSIT_CHANGE: { - return "TRANSIT_CHANGE"; - } - case TRANSIT_KEYGUARD_GOING_AWAY: { - return "TRANSIT_KEYGUARD_GOING_AWAY"; - } - case TRANSIT_KEYGUARD_OCCLUDE: { - return "TRANSIT_KEYGUARD_OCCLUDE"; - } - case TRANSIT_KEYGUARD_UNOCCLUDE: { - return "TRANSIT_KEYGUARD_UNOCCLUDE"; - } - default: { - return "<UNKNOWN: " + transition + ">"; - } - } - } - - private String appStateToString() { - switch (mAppTransitionState) { - case APP_STATE_IDLE: - return "APP_STATE_IDLE"; - case APP_STATE_READY: - return "APP_STATE_READY"; - case APP_STATE_RUNNING: - return "APP_STATE_RUNNING"; - case APP_STATE_TIMEOUT: - return "APP_STATE_TIMEOUT"; - default: - return "unknown state=" + mAppTransitionState; - } - } - - private String transitTypeToString() { - switch (mNextAppTransitionType) { - case NEXT_TRANSIT_TYPE_NONE: - return "NEXT_TRANSIT_TYPE_NONE"; - case NEXT_TRANSIT_TYPE_CUSTOM: - return "NEXT_TRANSIT_TYPE_CUSTOM"; - case NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE: - return "NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE"; - case NEXT_TRANSIT_TYPE_SCALE_UP: - return "NEXT_TRANSIT_TYPE_SCALE_UP"; - case NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP: - return "NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP"; - case NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN: - return "NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN"; - case NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP: - return "NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP"; - case NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN: - return "NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN"; - case NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS: - return "NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS"; - default: - return "unknown type=" + mNextAppTransitionType; - } - } - - private static final ArrayList<Pair<Integer, String>> sFlagToString; - - static { - sFlagToString = new ArrayList<>(); - sFlagToString.add(new Pair<>(TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE, - "TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE")); - sFlagToString.add(new Pair<>(TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION, - "TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION")); - sFlagToString.add(new Pair<>(TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER, - "TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER")); - sFlagToString.add(new Pair<>(TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION, - "TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION")); - sFlagToString.add(new Pair<>( - TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_LAUNCHER_CLEAR_SNAPSHOT, - "TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_LAUNCHER_WITH_IN_WINDOW_ANIMATIONS")); - sFlagToString.add(new Pair<>(TRANSIT_FLAG_APP_CRASHED, - "TRANSIT_FLAG_APP_CRASHED")); - sFlagToString.add(new Pair<>(TRANSIT_FLAG_OPEN_BEHIND, - "TRANSIT_FLAG_OPEN_BEHIND")); - } - - /** - * Returns the human readable names of transit flags. - * - * @param flags a bitmask combination of transit flags. - * @return The combination of symbolic names. - */ - public static String appTransitionFlagsToString(int flags) { - String sep = ""; - StringBuilder sb = new StringBuilder(); - for (Pair<Integer, String> pair : sFlagToString) { - if ((flags & pair.first) != 0) { - sb.append(sep); - sb.append(pair.second); - sep = " | "; - } - } - return sb.toString(); - } - - void dumpDebug(ProtoOutputStream proto, long fieldId) { - final long token = proto.start(fieldId); - proto.write(APP_TRANSITION_STATE, mAppTransitionState); - proto.write(LAST_USED_APP_TRANSITION, mLastUsedAppTransition); - proto.end(token); - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); pw.println(this); - pw.print(prefix); pw.print("mAppTransitionState="); pw.println(appStateToString()); - if (mNextAppTransitionType != NEXT_TRANSIT_TYPE_NONE) { - pw.print(prefix); pw.print("mNextAppTransitionType="); - pw.println(transitTypeToString()); - } - if (mNextAppTransitionOverrideRequested - || mNextAppTransitionType == NEXT_TRANSIT_TYPE_CUSTOM) { - pw.print(prefix); pw.print("mNextAppTransitionPackage="); - pw.println(mNextAppTransitionPackage); - pw.print(prefix); pw.print("mNextAppTransitionEnter=0x"); - pw.print(Integer.toHexString(mNextAppTransitionEnter)); - pw.print(" mNextAppTransitionExit=0x"); - pw.println(Integer.toHexString(mNextAppTransitionExit)); - pw.print(" mNextAppTransitionBackgroundColor=0x"); - pw.println(Integer.toHexString(mNextAppTransitionBackgroundColor)); - } - switch (mNextAppTransitionType) { - case NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE: - pw.print(prefix); pw.print("mNextAppTransitionPackage="); - pw.println(mNextAppTransitionPackage); - pw.print(prefix); pw.print("mNextAppTransitionInPlace=0x"); - pw.print(Integer.toHexString(mNextAppTransitionInPlace)); - break; - case NEXT_TRANSIT_TYPE_SCALE_UP: { - getDefaultNextAppTransitionStartRect(mTmpRect); - pw.print(prefix); pw.print("mNextAppTransitionStartX="); - pw.print(mTmpRect.left); - pw.print(" mNextAppTransitionStartY="); - pw.println(mTmpRect.top); - pw.print(prefix); pw.print("mNextAppTransitionStartWidth="); - pw.print(mTmpRect.width()); - pw.print(" mNextAppTransitionStartHeight="); - pw.println(mTmpRect.height()); - break; - } - case NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP: - case NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN: - case NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP: - case NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN: { - pw.print(prefix); pw.print("mDefaultNextAppTransitionAnimationSpec="); - pw.println(mDefaultNextAppTransitionAnimationSpec); - pw.print(prefix); pw.print("mNextAppTransitionAnimationsSpecs="); - pw.println(mNextAppTransitionAnimationsSpecs); - pw.print(prefix); pw.print("mNextAppTransitionScaleUp="); - pw.println(mNextAppTransitionScaleUp); - break; - } - } - if (mNextAppTransitionCallback != null) { - pw.print(prefix); pw.print("mNextAppTransitionCallback="); - pw.println(mNextAppTransitionCallback); - } - if (mLastUsedAppTransition != TRANSIT_OLD_NONE) { - pw.print(prefix); pw.print("mLastUsedAppTransition="); - pw.println(appTransitionOldToString(mLastUsedAppTransition)); - pw.print(prefix); pw.print("mLastOpeningApp="); - pw.println(mLastOpeningApp); - pw.print(prefix); pw.print("mLastClosingApp="); - pw.println(mLastClosingApp); - pw.print(prefix); pw.print("mLastChangingApp="); - pw.println(mLastChangingApp); - } - } - - boolean prepareAppTransition(@TransitionType int transit, @TransitionFlags int flags) { - if (WindowManagerService.sEnableShellTransitions) { - return false; - } - mNextAppTransitionRequests.add(transit); - mNextAppTransitionFlags |= flags; - updateBooster(); - removeAppTransitionTimeoutCallbacks(); - mHandler.postDelayed(mHandleAppTransitionTimeoutRunnable, - APP_TRANSITION_TIMEOUT_MS); - return prepare(); - } - - /** - * @return true if {@param transit} is representing a transition in which Keyguard is going - * away, false otherwise - */ - public static boolean isKeyguardGoingAwayTransitOld(int transit) { - return transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY - || transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER; - } - - static boolean isKeyguardOccludeTransitOld(@TransitionOldType int transit) { - return transit == TRANSIT_OLD_KEYGUARD_OCCLUDE - || transit == TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM - || transit == TRANSIT_OLD_KEYGUARD_UNOCCLUDE; - } - - static boolean isKeyguardTransitOld(@TransitionOldType int transit) { - return isKeyguardGoingAwayTransitOld(transit) || isKeyguardOccludeTransitOld(transit); - } - - static boolean isTaskTransitOld(@TransitionOldType int transit) { - return isTaskOpenTransitOld(transit) - || isTaskCloseTransitOld(transit); - } - - static boolean isTaskCloseTransitOld(@TransitionOldType int transit) { - return transit == TRANSIT_OLD_TASK_CLOSE - || transit == TRANSIT_OLD_TASK_TO_BACK; - } - - private static boolean isTaskOpenTransitOld(@TransitionOldType int transit) { - return transit == TRANSIT_OLD_TASK_OPEN - || transit == TRANSIT_OLD_TASK_OPEN_BEHIND - || transit == TRANSIT_OLD_TASK_TO_FRONT; - } - - static boolean isActivityTransitOld(@TransitionOldType int transit) { - return transit == TRANSIT_OLD_ACTIVITY_OPEN - || transit == TRANSIT_OLD_ACTIVITY_CLOSE - || transit == TRANSIT_OLD_ACTIVITY_RELAUNCH; - } - - static boolean isTaskFragmentTransitOld(@TransitionOldType int transit) { - return transit == TRANSIT_OLD_TASK_FRAGMENT_OPEN - || transit == TRANSIT_OLD_TASK_FRAGMENT_CLOSE - || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE; - } - - static boolean isChangeTransitOld(@TransitionOldType int transit) { - return transit == TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE - || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE; - } - - static boolean isClosingTransitOld(@TransitionOldType int transit) { - return transit == TRANSIT_OLD_ACTIVITY_CLOSE - || transit == TRANSIT_OLD_TASK_CLOSE - || transit == TRANSIT_OLD_WALLPAPER_CLOSE - || transit == TRANSIT_OLD_WALLPAPER_INTRA_CLOSE - || transit == TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE - || transit == TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE; - } - - static boolean isNormalTransit(@TransitionType int transit) { - return transit == TRANSIT_OPEN - || transit == TRANSIT_CLOSE - || transit == TRANSIT_TO_FRONT - || transit == TRANSIT_TO_BACK; - } - - static boolean isKeyguardTransit(@TransitionType int transit) { - return transit == TRANSIT_KEYGUARD_GOING_AWAY - || transit == TRANSIT_KEYGUARD_OCCLUDE - || transit == TRANSIT_KEYGUARD_UNOCCLUDE; - } - - @TransitionType int getKeyguardTransition() { - if (mNextAppTransitionRequests.indexOf(TRANSIT_KEYGUARD_GOING_AWAY) != -1) { - return TRANSIT_KEYGUARD_GOING_AWAY; - } - final int unoccludeIndex = mNextAppTransitionRequests.indexOf(TRANSIT_KEYGUARD_UNOCCLUDE); - final int occludeIndex = mNextAppTransitionRequests.indexOf(TRANSIT_KEYGUARD_OCCLUDE); - // No keyguard related transition requests. - if (unoccludeIndex == -1 && occludeIndex == -1) { - return TRANSIT_NONE; - } - // In case we unocclude Keyguard and occlude it again, meaning that we never actually - // unoccclude/occlude Keyguard, but just run a normal transition. - if (unoccludeIndex != -1 && unoccludeIndex < occludeIndex) { - return TRANSIT_NONE; - } - return unoccludeIndex != -1 ? TRANSIT_KEYGUARD_UNOCCLUDE : TRANSIT_KEYGUARD_OCCLUDE; - } - - @TransitionType int getFirstAppTransition() { - for (int i = 0; i < mNextAppTransitionRequests.size(); ++i) { - final @TransitionType int transit = mNextAppTransitionRequests.get(i); - if (transit != TRANSIT_NONE && !isKeyguardTransit(transit)) { - return transit; - } - } - return TRANSIT_NONE; - } - - boolean containsTransitRequest(@TransitionType int transit) { - return mNextAppTransitionRequests.contains(transit); - } - - private void handleAppTransitionTimeout() { - } - - private static void doAnimationCallback(@NonNull IRemoteCallback callback) { - try { - ((IRemoteCallback) callback).sendResult(null); - } catch (RemoteException e) { - } - } - - private void setAppTransitionFinishedCallbackIfNeeded(Animation anim) { - final IRemoteCallback callback = mAnimationFinishedCallback; - if (callback != null && anim != null) { - anim.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { } - - @Override - public void onAnimationEnd(Animation animation) { - mHandler.sendMessage(PooledLambda.obtainMessage( - AppTransition::doAnimationCallback, callback)); - } - - @Override - public void onAnimationRepeat(Animation animation) { } - }); - } - } - - void removeAppTransitionTimeoutCallbacks() { - mHandler.removeCallbacks(mHandleAppTransitionTimeoutRunnable); - } -} diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index e9d9ace825c0..e9b7649e8cbd 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -691,8 +691,8 @@ class BackNavigationController { return false; } if (window.mAttrs.windowAnimations != 0) { - final TransitionAnimation transitionAnimation = window.getDisplayContent() - .mAppTransition.mTransitionAnimation; + final TransitionAnimation transitionAnimation = window.mDisplayContent + .mTransitionAnimation; final int attr = com.android.internal.R.styleable .WindowAnimation_activityCloseExitAnimation; final int appResId = transitionAnimation.getAnimationResId( diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index e134e271f6dc..530b507bcc0f 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -99,7 +99,6 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SCREEN_ON; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WALLPAPER; import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS; import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN; -import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT; @@ -155,7 +154,6 @@ import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELD import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields; import static com.android.server.wm.utils.RegionUtils.forEachRectReverse; import static com.android.server.wm.utils.RegionUtils.rectListToRegion; -import static com.android.window.flags.Flags.enablePersistingDensityScaleForConnectedDisplays; import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays; import android.annotation.IntDef; @@ -236,6 +234,7 @@ import android.view.WindowManager; import android.view.WindowManager.DisplayImePolicy; import android.view.WindowManagerPolicyConstants.PointerEventListener; import android.view.inputmethod.ImeTracker; +import android.window.DesktopExperienceFlags; import android.window.DisplayWindowPolicyController; import android.window.IDisplayAreaOrganizer; import android.window.ScreenCapture; @@ -247,6 +246,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.ToBooleanFunction; import com.android.internal.util.function.pooled.PooledLambda; @@ -364,7 +364,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp private boolean mTmpInitial; private int mMaxUiWidth = 0; - final AppTransition mAppTransition; + // TODO(b/400335290): extract the needed methods and remove this field. + final TransitionAnimation mTransitionAnimation; final UnknownAppVisibilityController mUnknownAppVisibilityController; @@ -1179,7 +1180,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mHoldScreenWakeLock.setReferenceCounted(false); mFixedRotationTransitionListener = new FixedRotationTransitionListener(mDisplayId); - mAppTransition = new AppTransition(mWmService.mContext, mWmService, this); + mTransitionAnimation = new TransitionAnimation(mWmService.mContext, false /* debug */, TAG); mTransitionController.registerLegacyListener(mFixedRotationTransitionListener); mUnknownAppVisibilityController = new UnknownAppVisibilityController(mWmService, this); mRemoteDisplayChangeController = new RemoteDisplayChangeController(this); @@ -3153,7 +3154,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } // Update the base density if there is a forced density ratio. - if (enablePersistingDensityScaleForConnectedDisplays() + if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue() && mIsDensityForced && mExternalDisplayForcedDensityRatio != 0.0f) { mBaseDisplayDensity = (int) (mInitialDisplayDensity * mExternalDisplayForcedDensityRatio + 0.5); @@ -3188,7 +3189,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp density = 0; } // Save the new density ratio to settings for external displays. - if (enablePersistingDensityScaleForConnectedDisplays() + if (DesktopExperienceFlags.ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS.isTrue() && mDisplayInfo.type == TYPE_EXTERNAL) { mExternalDisplayForcedDensityRatio = (float) mBaseDisplayDensity / getInitialDisplayDensity(); @@ -3257,7 +3258,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } void onDisplayInfoChangeApplied() { - if (!enableDisplayContentModeManagement()) { + if (!DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) { Slog.e(TAG, "ShouldShowSystemDecors shouldn't be updated when the flag is off."); } @@ -3279,6 +3280,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * Whether the display is allowed to switch the content mode between extended and mirroring. + * If the content mode is extended, the display will start home activity and show system + * decorations, such as wallpapaer, status bar and navigation bar. + * If the content mode is mirroring, the display will not show home activity or system + * decorations. + * The content mode is switched when {@link Display#canHostTasks()} changes. + * * Note that we only allow displays that are able to show system decorations to use the content * mode switch; however, not all displays that are able to show system decorations are allowed * to use the content mode switch. diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index ec5b503fbb9b..313b77ed4b20 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -67,7 +67,6 @@ import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SCREEN_ON; -import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT; import static com.android.server.policy.WindowManagerPolicy.TRANSIT_PREVIEW_DONE; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT; @@ -107,6 +106,7 @@ import android.view.InsetsFlags; import android.view.InsetsFrameProvider; import android.view.InsetsSource; import android.view.InsetsState; +import android.view.PrivacyIndicatorBounds; import android.view.Surface; import android.view.View; import android.view.ViewDebug; @@ -119,6 +119,7 @@ import android.view.WindowManager.LayoutParams; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.window.ClientWindowFrames; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import com.android.internal.R; @@ -747,7 +748,7 @@ public class DisplayPolicy { } void updateHasNavigationBarIfNeeded() { - if (!enableDisplayContentModeManagement()) { + if (!DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) { Slog.e(TAG, "mHasNavigationBar shouldn't be updated when the flag is off."); } @@ -1875,7 +1876,7 @@ public class DisplayPolicy { } void notifyDisplayAddSystemDecorations() { - if (enableDisplayContentModeManagement()) { + if (DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) { final int displayId = getDisplayId(); final boolean isSystemDecorationsSupported = mDisplayContent.isSystemDecorationsSupported(); @@ -2121,6 +2122,8 @@ public class DisplayPolicy { } private static class Cache { + static final int TYPE_REGULAR_BARS = WindowInsets.Type.statusBars() + | WindowInsets.Type.navigationBars(); /** * If {@link #mPreserveId} is this value, it is in the middle of updating display * configuration before a transition is started. Then the active cache should be used. @@ -2130,6 +2133,14 @@ public class DisplayPolicy { int mPreserveId; boolean mActive; + /** + * When display switches, mRegularBarsInsets will assign to mPreservedInsets, and the + * insets sources of previous device state will copy to mRegularBarsInsets. + */ + ArrayList<InsetsSource> mPreservedInsets; + ArrayList<InsetsSource> mRegularBarsInsets; + PrivacyIndicatorBounds mPrivacyIndicatorBounds; + Cache(DisplayContent dc) { mDecorInsets = new DecorInsets(dc); } @@ -2138,6 +2149,17 @@ public class DisplayPolicy { return mPreserveId == ID_UPDATING_CONFIG || mDecorInsets.mDisplayContent .mTransitionController.inTransition(mPreserveId); } + + static ArrayList<InsetsSource> copyRegularBarInsets(InsetsState srcState) { + final ArrayList<InsetsSource> state = new ArrayList<>(); + for (int i = srcState.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = srcState.sourceAt(i); + if ((source.getType() & TYPE_REGULAR_BARS) != 0) { + state.add(new InsetsSource(source)); + } + } + return state; + } } } @@ -2213,24 +2235,60 @@ public class DisplayPolicy { @VisibleForTesting void updateCachedDecorInsets() { DecorInsets prevCache = null; + PrivacyIndicatorBounds privacyIndicatorBounds = null; if (mCachedDecorInsets == null) { mCachedDecorInsets = new DecorInsets.Cache(mDisplayContent); } else { prevCache = new DecorInsets(mDisplayContent); prevCache.setTo(mCachedDecorInsets.mDecorInsets); + privacyIndicatorBounds = mCachedDecorInsets.mPrivacyIndicatorBounds; + mCachedDecorInsets.mPreservedInsets = mCachedDecorInsets.mRegularBarsInsets; } // Set a special id to preserve it before a real id is available from transition. mCachedDecorInsets.mPreserveId = DecorInsets.Cache.ID_UPDATING_CONFIG; // Cache the current insets. mCachedDecorInsets.mDecorInsets.setTo(mDecorInsets); + if (com.android.window.flags.Flags.useCachedInsetsForDisplaySwitch()) { + mCachedDecorInsets.mRegularBarsInsets = DecorInsets.Cache.copyRegularBarInsets( + mDisplayContent.mDisplayFrames.mInsetsState); + mCachedDecorInsets.mPrivacyIndicatorBounds = + mDisplayContent.mCurrentPrivacyIndicatorBounds; + } else { + mCachedDecorInsets.mRegularBarsInsets = null; + mCachedDecorInsets.mPrivacyIndicatorBounds = null; + } // Switch current to previous cache. if (prevCache != null) { mDecorInsets.setTo(prevCache); + if (privacyIndicatorBounds != null) { + mDisplayContent.mCurrentPrivacyIndicatorBounds = privacyIndicatorBounds; + } mCachedDecorInsets.mActive = true; } } /** + * This returns a new InsetsState with replacing the insets in target device state when the + * display is switching (e.g. fold/unfold). Otherwise, it returns the original state. This is + * to avoid dispatching old insets source before the insets providers update new insets. + */ + InsetsState replaceInsetsSourcesIfNeeded(InsetsState originalState, boolean copyState) { + if (mCachedDecorInsets == null || mCachedDecorInsets.mPreservedInsets == null + || !shouldKeepCurrentDecorInsets()) { + return originalState; + } + final ArrayList<InsetsSource> preservedSources = mCachedDecorInsets.mPreservedInsets; + final InsetsState state = copyState ? new InsetsState(originalState) : originalState; + for (int i = preservedSources.size() - 1; i >= 0; i--) { + final InsetsSource cacheSource = preservedSources.get(i); + if (state.peekSource(cacheSource.getId()) != null) { + state.addSource(new InsetsSource(cacheSource)); + } + } + return state; + } + + /** * Called after the display configuration is updated according to the physical change. Suppose * there should be a display change transition, so associate the cached decor insets with the * transition to limit the lifetime of using the cache. diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index c6892e941fc6..48ffccbca935 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -23,7 +23,6 @@ import static android.view.WindowManager.REMOVE_CONTENT_MODE_DESTROY; import static android.view.WindowManager.REMOVE_CONTENT_MODE_MOVE_TO_PRIMARY; import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED; -import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.server.wm.DisplayContent.FORCE_SCALING_MODE_AUTO; import static com.android.server.wm.DisplayContent.FORCE_SCALING_MODE_DISABLED; @@ -37,6 +36,7 @@ import android.view.IWindowManager; import android.view.Surface; import android.view.WindowManager; import android.view.WindowManager.DisplayImePolicy; +import android.window.DesktopExperienceFlags; import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.DisplayContent.ForceScalingMode; @@ -255,7 +255,7 @@ class DisplayWindowSettings { final boolean changed = (shouldShow != shouldShowSystemDecorsLocked(dc)); setShouldShowSystemDecorsInternalLocked(dc, shouldShow); - if (enableDisplayContentModeManagement()) { + if (DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) { if (dc.isDefaultDisplay || dc.isPrivate() || !changed) { return; } diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 2a2ae12ab09b..2cac63c1e5e9 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -104,7 +104,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { if (!mGivenInsetsReady && isServerVisible() && !givenInsetsPending && mControlTarget != null) { ProtoLog.d(WM_DEBUG_IME, - "onPostLayout: IME control ready to be dispatched, ws=%s", ws); + "onPostLayout: IME control ready to be dispatched, controlTarget=%s", + mControlTarget); mGivenInsetsReady = true; ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED); @@ -115,13 +116,15 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider { // If the server visibility didn't change (still visible), and mGivenInsetsReady // is set, we won't call into notifyControlChanged. Therefore, we can reset the // statsToken, if available. - ProtoLog.w(WM_DEBUG_IME, "onPostLayout cancel statsToken, ws=%s", ws); + ProtoLog.w(WM_DEBUG_IME, "onPostLayout cancel statsToken, controlTarget=%s", + mControlTarget); ImeTracker.forLogging().onCancelled(mStatsToken, ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED); mStatsToken = null; } else if (wasServerVisible && !isServerVisible()) { - ProtoLog.d(WM_DEBUG_IME, "onPostLayout: setImeShowing(false) was: %s, ws=%s", - isImeShowing(), ws); + ProtoLog.d(WM_DEBUG_IME, + "onPostLayout: setImeShowing(false) was: %s, controlTarget=%s", + isImeShowing(), mControlTarget); setImeShowing(false); } } diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 28722141dcd3..06754c4517ab 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -229,6 +229,7 @@ class InsetsPolicy { state = originalState; } state = adjustVisibilityForIme(target, state, state == originalState); + state = mPolicy.replaceInsetsSourcesIfNeeded(state, state == originalState); return adjustInsetsForRoundedCorners(target.mToken, state, state == originalState); } diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java index 29c0c7b2035e..3a4d2ca8b9bd 100644 --- a/services/core/java/com/android/server/wm/Letterbox.java +++ b/services/core/java/com/android/server/wm/Letterbox.java @@ -54,7 +54,6 @@ public class Letterbox { private final Supplier<SurfaceControl.Builder> mSurfaceControlFactory; private final Supplier<SurfaceControl.Transaction> mTransactionFactory; - private final Supplier<SurfaceControl> mParentSurfaceSupplier; private final Rect mOuter = new Rect(); private final Rect mInner = new Rect(); @@ -83,13 +82,11 @@ public class Letterbox { public Letterbox(Supplier<SurfaceControl.Builder> surfaceControlFactory, Supplier<SurfaceControl.Transaction> transactionFactory, @NonNull AppCompatReachabilityPolicy appCompatReachabilityPolicy, - @NonNull AppCompatLetterboxOverrides appCompatLetterboxOverrides, - Supplier<SurfaceControl> parentSurface) { + @NonNull AppCompatLetterboxOverrides appCompatLetterboxOverrides) { mSurfaceControlFactory = surfaceControlFactory; mTransactionFactory = transactionFactory; mAppCompatReachabilityPolicy = appCompatReachabilityPolicy; mAppCompatLetterboxOverrides = appCompatLetterboxOverrides; - mParentSurfaceSupplier = parentSurface; } /** @@ -343,7 +340,6 @@ public class Letterbox { private SurfaceControl mInputSurface; private Color mColor; private boolean mHasWallpaperBackground; - private SurfaceControl mParentSurface; private final Rect mSurfaceFrameRelative = new Rect(); private final Rect mLayoutFrameGlobal = new Rect(); @@ -437,9 +433,8 @@ public class Letterbox { } mColor = mAppCompatLetterboxOverrides.getLetterboxBackgroundColor(); - mParentSurface = mParentSurfaceSupplier.get(); t.setColor(mSurface, getRgbColorArray()); - setPositionAndReparent(t, mSurface); + setPositionAndCrop(t, mSurface); mHasWallpaperBackground = mAppCompatLetterboxOverrides .hasWallpaperBackgroundForLetterbox(); @@ -448,7 +443,7 @@ public class Letterbox { t.show(mSurface); if (mInputSurface != null) { - setPositionAndReparent(inputT, mInputSurface); + setPositionAndCrop(inputT, mInputSurface); inputT.setTrustedOverlay(mInputSurface, true); inputT.show(mInputSurface); } @@ -470,12 +465,11 @@ public class Letterbox { } } - private void setPositionAndReparent(@NonNull SurfaceControl.Transaction t, + private void setPositionAndCrop(@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl surface) { t.setPosition(surface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top); t.setWindowCrop(surface, mSurfaceFrameRelative.width(), mSurfaceFrameRelative.height()); - t.reparent(surface, mParentSurface); } private void updateAlphaAndBlur(SurfaceControl.Transaction t) { @@ -511,14 +505,13 @@ public class Letterbox { public boolean needsApplySurfaceChanges() { return !mSurfaceFrameRelative.equals(mLayoutFrameRelative) - // If mSurfaceFrameRelative is empty then mHasWallpaperBackground, mColor, - // and mParentSurface may never be updated in applySurfaceChanges but this - // doesn't mean that update is needed. + // If mSurfaceFrameRelative is empty, then mHasWallpaperBackground and mColor + // may never be updated in applySurfaceChanges but this doesn't mean that + // update is needed. || !mSurfaceFrameRelative.isEmpty() && (mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox() != mHasWallpaperBackground - || !mAppCompatLetterboxOverrides.getLetterboxBackgroundColor().equals(mColor) - || mParentSurfaceSupplier.get() != mParentSurface); + || !mAppCompatLetterboxOverrides.getLetterboxBackgroundColor().equals(mColor)); } } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index e864ecf60770..394fe6f0e8ed 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -36,6 +36,7 @@ import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_WAKE; +import static android.window.DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_FOCUS_LIGHT; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_KEEP_SCREEN_ON; @@ -45,7 +46,6 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_STATES; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WALLPAPER; import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_SURFACE_ALLOC; -import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement; import static com.android.server.policy.PhoneWindowManager.SYSTEM_DIALOG_REASON_ASSIST; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT; import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER; @@ -1371,7 +1371,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // When display content mode management flag is enabled, the task display area is marked as // removed when switching from extended display to mirroring display. We need to restart the // task display area before starting the home. - if (enableDisplayContentModeManagement() && taskDisplayArea.shouldKeepNoTask()) { + if (ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue() + && taskDisplayArea.shouldKeepNoTask()) { taskDisplayArea.setShouldKeepNoTask(false); } @@ -2771,7 +2772,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> return; } - if (enableDisplayContentModeManagement() && display.allowContentModeSwitch()) { + if (ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue() + && display.allowContentModeSwitch()) { mWindowManager.mDisplayWindowSettings .setShouldShowSystemDecorsInternalLocked(display, display.mDisplay.canHostTasks()); @@ -2823,7 +2825,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> displayContent.requestDisplayUpdate( () -> { clearDisplayInfoCaches(displayId); - if (enableDisplayContentModeManagement()) { + if (ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue()) { displayContent.onDisplayInfoChangeApplied(); } }); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index f95698a5b0bd..5183c6b57f15 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -88,6 +88,7 @@ import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; +import android.os.RemoteException; import android.os.UserHandle; import android.util.ArraySet; import android.util.DisplayMetrics; @@ -1751,67 +1752,80 @@ class TaskFragment extends WindowContainer<WindowContainer> { } } - try { - final IApplicationThread appThread = next.app.getThread(); - // Deliver all pending results. - final ArrayList<ResultInfo> a = next.results; - if (a != null) { - final int size = a.size(); - if (!next.finishing && size > 0) { - if (DEBUG_RESULTS) { - Slog.v(TAG_RESULTS, "Delivering results to " + next + ": " + a); - } - final ActivityResultItem item = new ActivityResultItem(next.token, a); - mAtmService.getLifecycleManager().scheduleTransactionItem(appThread, item); + final IApplicationThread appThread = next.app.getThread(); + // Deliver all pending results. + final ArrayList<ResultInfo> a = next.results; + if (a != null) { + final int size = a.size(); + if (!next.finishing && size > 0) { + if (DEBUG_RESULTS) { + Slog.v(TAG_RESULTS, "Delivering results to " + next + ": " + a); + } + final ActivityResultItem item = new ActivityResultItem(next.token, a); + boolean isSuccessful; + try { + isSuccessful = mAtmService.getLifecycleManager().scheduleTransactionItem( + appThread, item); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup + isSuccessful = false; + } + if (!isSuccessful) { + onResumeTopActivityRemoteFailure(lastState, next, lastResumedActivity, + lastFocusedRootTask); + return true; } } + } - if (next.newIntents != null) { - final NewIntentItem item = - new NewIntentItem(next.token, next.newIntents, true /* resume */); - mAtmService.getLifecycleManager().scheduleTransactionItem(appThread, item); + if (next.newIntents != null) { + final NewIntentItem item = + new NewIntentItem(next.token, next.newIntents, true /* resume */); + boolean isSuccessful; + try { + isSuccessful = mAtmService.getLifecycleManager().scheduleTransactionItem( + appThread, item); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup + isSuccessful = false; } + if (!isSuccessful) { + onResumeTopActivityRemoteFailure(lastState, next, lastResumedActivity, + lastFocusedRootTask); + return true; + } + } - // Well the app will no longer be stopped. - // Clear app token stopped state in window manager if needed. - next.notifyAppResumed(); - - EventLogTags.writeWmResumeActivity(next.mUserId, System.identityHashCode(next), - next.getTask().mTaskId, next.shortComponentName); - - mAtmService.getAppWarningsLocked().onResumeActivity(next); - final int topProcessState = mAtmService.mTopProcessState; - next.app.setPendingUiCleanAndForceProcessStateUpTo(topProcessState); - next.abortAndClearOptionsAnimation(); - final ResumeActivityItem resumeActivityItem = new ResumeActivityItem( - next.token, topProcessState, dc.isNextTransitionForward(), - next.shouldSendCompatFakeFocus()); - mAtmService.getLifecycleManager().scheduleTransactionItem( - appThread, resumeActivityItem); - - ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Resumed %s", next); - } catch (Exception e) { - // Whoops, need to restart this activity! - ProtoLog.v(WM_DEBUG_STATES, "Resume failed; resetting state to %s: " - + "%s", lastState, next); - next.setState(lastState, "resumeTopActivityInnerLocked"); + // Well the app will no longer be stopped. + // Clear app token stopped state in window manager if needed. + next.notifyAppResumed(); - // lastResumedActivity being non-null implies there is a lastStack present. - if (lastResumedActivity != null) { - lastResumedActivity.setState(RESUMED, "resumeTopActivityInnerLocked"); - } + EventLogTags.writeWmResumeActivity(next.mUserId, System.identityHashCode(next), + next.getTask().mTaskId, next.shortComponentName); - Slog.i(TAG, "Restarting because process died: " + next); - if (!next.hasBeenLaunched) { - next.hasBeenLaunched = true; - } else if (SHOW_APP_STARTING_PREVIEW && lastFocusedRootTask != null - && lastFocusedRootTask.isTopRootTaskInDisplayArea()) { - next.showStartingWindow(false /* taskSwitch */); - } - mTaskSupervisor.startSpecificActivity(next, true, false); + mAtmService.getAppWarningsLocked().onResumeActivity(next); + final int topProcessState = mAtmService.mTopProcessState; + next.app.setPendingUiCleanAndForceProcessStateUpTo(topProcessState); + next.abortAndClearOptionsAnimation(); + final ResumeActivityItem resumeActivityItem = new ResumeActivityItem( + next.token, topProcessState, dc.isNextTransitionForward(), + next.shouldSendCompatFakeFocus()); + boolean isSuccessful; + try { + isSuccessful = mAtmService.getLifecycleManager().scheduleTransactionItem( + appThread, resumeActivityItem); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup + isSuccessful = false; + } + if (!isSuccessful) { + onResumeTopActivityRemoteFailure(lastState, next, lastResumedActivity, + lastFocusedRootTask); return true; } + ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Resumed %s", next); + next.completeResumeLocked(); } else { // Whoops, need to restart this activity! @@ -1830,6 +1844,29 @@ class TaskFragment extends WindowContainer<WindowContainer> { return true; } + /** Likely app process has been killed. Needs to restart this activity. */ + private void onResumeTopActivityRemoteFailure(@NonNull ActivityRecord.State lastState, + @NonNull ActivityRecord next, @Nullable ActivityRecord lastResumedActivity, + @Nullable Task lastFocusedRootTask) { + ProtoLog.v(WM_DEBUG_STATES, "Resume failed; resetting state to %s: " + + "%s", lastState, next); + next.setState(lastState, "resumeTopActivityInnerLocked"); + + // lastResumedActivity being non-null implies there is a lastStack present. + if (lastResumedActivity != null) { + lastResumedActivity.setState(RESUMED, "resumeTopActivityInnerLocked"); + } + + Slog.i(TAG, "Restarting because process died: " + next); + if (!next.hasBeenLaunched) { + next.hasBeenLaunched = true; + } else if (SHOW_APP_STARTING_PREVIEW && lastFocusedRootTask != null + && lastFocusedRootTask.isTopRootTaskInDisplayArea()) { + next.showStartingWindow(false /* taskSwitch */); + } + mTaskSupervisor.startSpecificActivity(next, true, false); + } + boolean shouldSleepOrShutDownActivities() { return shouldSleepActivities() || mAtmService.mShuttingDown; } @@ -2034,17 +2071,23 @@ class TaskFragment extends WindowContainer<WindowContainer> { void schedulePauseActivity(ActivityRecord prev, boolean userLeaving, boolean pauseImmediately, boolean autoEnteringPip, String reason) { ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev); + prev.mPauseSchedulePendingForPip = false; + EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev), + prev.shortComponentName, "userLeaving=" + userLeaving, reason); + + final PauseActivityItem item = new PauseActivityItem(prev.token, prev.finishing, + userLeaving, pauseImmediately, autoEnteringPip); + boolean isSuccessful; try { - prev.mPauseSchedulePendingForPip = false; - EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev), - prev.shortComponentName, "userLeaving=" + userLeaving, reason); - - final PauseActivityItem item = new PauseActivityItem(prev.token, prev.finishing, - userLeaving, pauseImmediately, autoEnteringPip); - mAtmService.getLifecycleManager().scheduleTransactionItem(prev.app.getThread(), item); - } catch (Exception e) { + isSuccessful = mAtmService.getLifecycleManager().scheduleTransactionItem( + prev.app.getThread(), item); + } catch (RemoteException e) { + // TODO(b/323801078): remove Exception when cleanup // Ignore exception, if process died other code will cleanup. Slog.w(TAG, "Exception thrown during pause", e); + isSuccessful = false; + } + if (!isSuccessful) { mPausingActivity = null; mLastPausedActivity = null; mTaskSupervisor.mNoHistoryActivities.remove(prev); diff --git a/services/core/java/com/android/server/wm/ViewServer.java b/services/core/java/com/android/server/wm/ViewServer.java index ecf5652a1e69..971e6f95d6bd 100644 --- a/services/core/java/com/android/server/wm/ViewServer.java +++ b/services/core/java/com/android/server/wm/ViewServer.java @@ -19,7 +19,10 @@ package com.android.server.wm; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import static com.android.server.wm.WindowManagerInternal.WindowFocusChangeListener; +import static com.android.server.wm.WindowManagerService.WindowChangeListener; +import android.os.IBinder; import android.util.Slog; import java.net.ServerSocket; @@ -206,7 +209,7 @@ class ViewServer implements Runnable { return result; } - class ViewServerWorker implements Runnable, WindowManagerService.WindowChangeListener { + class ViewServerWorker implements Runnable, WindowChangeListener, WindowFocusChangeListener { private Socket mClient; private boolean mNeedWindowListUpdate; private boolean mNeedFocusedWindowUpdate; @@ -284,7 +287,7 @@ class ViewServer implements Runnable { } } - public void focusChanged() { + public void focusChanged(IBinder focusedWindowToken) { synchronized(this) { mNeedFocusedWindowUpdate = true; notifyAll(); @@ -293,6 +296,7 @@ class ViewServer implements Runnable { private boolean windowManagerAutolistLoop() { mWindowManager.addWindowChangeListener(this); + mWindowManager.addWindowFocusChangeListener(this); BufferedWriter out = null; try { out = new BufferedWriter(new OutputStreamWriter(mClient.getOutputStream())); @@ -332,6 +336,7 @@ class ViewServer implements Runnable { } } mWindowManager.removeWindowChangeListener(this); + mWindowManager.removeWindowFocusChangeListener(this); } return true; } diff --git a/services/core/java/com/android/server/wm/WindowChangeAnimationSpec.java b/services/core/java/com/android/server/wm/WindowChangeAnimationSpec.java deleted file mode 100644 index d3530c50348a..000000000000 --- a/services/core/java/com/android/server/wm/WindowChangeAnimationSpec.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static com.android.server.wm.AnimationAdapter.STATUS_BAR_TRANSITION_DURATION; -import static com.android.server.wm.AnimationSpecProto.WINDOW; -import static com.android.server.wm.WindowAnimationSpecProto.ANIMATION; - -import android.graphics.Matrix; -import android.graphics.Rect; -import android.os.SystemClock; -import android.util.proto.ProtoOutputStream; -import android.view.DisplayInfo; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; -import android.view.animation.AlphaAnimation; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; -import android.view.animation.ClipRectAnimation; -import android.view.animation.ScaleAnimation; -import android.view.animation.Transformation; -import android.view.animation.TranslateAnimation; - -import com.android.server.wm.LocalAnimationAdapter.AnimationSpec; - -import java.io.PrintWriter; - -/** - * Animation spec for changing window animations. - */ -public class WindowChangeAnimationSpec implements AnimationSpec { - - private final ThreadLocal<TmpValues> mThreadLocalTmps = ThreadLocal.withInitial(TmpValues::new); - private final boolean mIsAppAnimation; - private final Rect mStartBounds; - private final Rect mEndBounds; - private final Rect mTmpRect = new Rect(); - - private Animation mAnimation; - private final boolean mIsThumbnail; - - static final int ANIMATION_DURATION = AppTransition.DEFAULT_APP_TRANSITION_DURATION; - - public WindowChangeAnimationSpec(Rect startBounds, Rect endBounds, DisplayInfo displayInfo, - float durationScale, boolean isAppAnimation, boolean isThumbnail) { - mStartBounds = new Rect(startBounds); - mEndBounds = new Rect(endBounds); - mIsAppAnimation = isAppAnimation; - mIsThumbnail = isThumbnail; - createBoundsInterpolator((int) (ANIMATION_DURATION * durationScale), displayInfo); - } - - @Override - public boolean getShowWallpaper() { - return false; - } - - @Override - public long getDuration() { - return mAnimation.getDuration(); - } - - /** - * This animator behaves slightly differently depending on whether the window is growing - * or shrinking: - * If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old) - * snapshot. - * If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker - * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into - * place. - * @param duration - * @param displayInfo - */ - private void createBoundsInterpolator(long duration, DisplayInfo displayInfo) { - boolean growing = mEndBounds.width() - mStartBounds.width() - + mEndBounds.height() - mStartBounds.height() >= 0; - float scalePart = 0.7f; - long scalePeriod = (long) (duration * scalePart); - float startScaleX = scalePart * ((float) mStartBounds.width()) / mEndBounds.width() - + (1.f - scalePart); - float startScaleY = scalePart * ((float) mStartBounds.height()) / mEndBounds.height() - + (1.f - scalePart); - if (mIsThumbnail) { - AnimationSet animSet = new AnimationSet(true); - Animation anim = new AlphaAnimation(1.f, 0.f); - anim.setDuration(scalePeriod); - if (!growing) { - anim.setStartOffset(duration - scalePeriod); - } - animSet.addAnimation(anim); - float endScaleX = 1.f / startScaleX; - float endScaleY = 1.f / startScaleY; - anim = new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY); - anim.setDuration(duration); - animSet.addAnimation(anim); - mAnimation = animSet; - mAnimation.initialize(mStartBounds.width(), mStartBounds.height(), - mEndBounds.width(), mEndBounds.height()); - } else { - AnimationSet animSet = new AnimationSet(true); - final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1); - scaleAnim.setDuration(scalePeriod); - if (!growing) { - scaleAnim.setStartOffset(duration - scalePeriod); - } - animSet.addAnimation(scaleAnim); - final Animation translateAnim = new TranslateAnimation(mStartBounds.left, - mEndBounds.left, mStartBounds.top, mEndBounds.top); - translateAnim.setDuration(duration); - animSet.addAnimation(translateAnim); - Rect startClip = new Rect(mStartBounds); - Rect endClip = new Rect(mEndBounds); - startClip.offsetTo(0, 0); - endClip.offsetTo(0, 0); - final Animation clipAnim = new ClipRectAnimation(startClip, endClip); - clipAnim.setDuration(duration); - animSet.addAnimation(clipAnim); - mAnimation = animSet; - mAnimation.initialize(mStartBounds.width(), mStartBounds.height(), - displayInfo.appWidth, displayInfo.appHeight); - } - } - - @Override - public void apply(Transaction t, SurfaceControl leash, long currentPlayTime) { - final TmpValues tmp = mThreadLocalTmps.get(); - if (mIsThumbnail) { - mAnimation.getTransformation(currentPlayTime, tmp.mTransformation); - t.setMatrix(leash, tmp.mTransformation.getMatrix(), tmp.mFloats); - t.setAlpha(leash, tmp.mTransformation.getAlpha()); - } else { - mAnimation.getTransformation(currentPlayTime, tmp.mTransformation); - final Matrix matrix = tmp.mTransformation.getMatrix(); - t.setMatrix(leash, matrix, tmp.mFloats); - - // The following applies an inverse scale to the clip-rect so that it crops "after" the - // scale instead of before. - tmp.mVecs[1] = tmp.mVecs[2] = 0; - tmp.mVecs[0] = tmp.mVecs[3] = 1; - matrix.mapVectors(tmp.mVecs); - tmp.mVecs[0] = 1.f / tmp.mVecs[0]; - tmp.mVecs[3] = 1.f / tmp.mVecs[3]; - final Rect clipRect = tmp.mTransformation.getClipRect(); - mTmpRect.left = (int) (clipRect.left * tmp.mVecs[0] + 0.5f); - mTmpRect.right = (int) (clipRect.right * tmp.mVecs[0] + 0.5f); - mTmpRect.top = (int) (clipRect.top * tmp.mVecs[3] + 0.5f); - mTmpRect.bottom = (int) (clipRect.bottom * tmp.mVecs[3] + 0.5f); - t.setWindowCrop(leash, mTmpRect); - } - } - - @Override - public long calculateStatusBarTransitionStartTime() { - long uptime = SystemClock.uptimeMillis(); - return Math.max(uptime, uptime + ((long) (((float) mAnimation.getDuration()) * 0.99f)) - - STATUS_BAR_TRANSITION_DURATION); - } - - @Override - public boolean canSkipFirstFrame() { - return false; - } - - @Override - public boolean needsEarlyWakeup() { - return mIsAppAnimation; - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); pw.println(mAnimation.getDuration()); - } - - @Override - public void dumpDebugInner(ProtoOutputStream proto) { - final long token = proto.start(WINDOW); - proto.write(ANIMATION, mAnimation.toString()); - proto.end(token); - } - - private static class TmpValues { - final Transformation mTransformation = new Transformation(); - final float[] mFloats = new float[9]; - final float[] mVecs = new float[4]; - } -} diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index e3746f18dca0..466ed7863c84 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -97,7 +97,6 @@ import com.android.server.wm.SurfaceAnimator.Animatable; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; import com.android.server.wm.utils.AlwaysTruePredicate; -import com.android.window.flags.Flags; import java.io.PrintWriter; import java.lang.ref.WeakReference; @@ -2630,7 +2629,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (!mTransitionController.canAssignLayers(this)) return; final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null; if (mSurfaceControl != null && changed) { - if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + if (mSyncState != SYNC_STATE_NONE) { // When this container needs to be synced, assign layer with its own sync // transaction to avoid out of ordering when merge. // Still use the passed-in transaction for non-sync case, such as building finish @@ -2647,7 +2646,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< boolean forceUpdate) { final boolean changed = layer != mLastLayer || mLastRelativeToLayer != relativeTo; if (mSurfaceControl != null && (changed || forceUpdate)) { - if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + if (mSyncState != SYNC_STATE_NONE) { // When this container needs to be synced, assign layer with its own sync // transaction to avoid out of ordering when merge. // Still use the passed-in transaction for non-sync case, such as building finish diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 6e224f07fcdc..4b5a3a031931 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -152,6 +152,30 @@ public abstract class WindowManagerInternal { } } + /** Interface for clients to receive callbacks related to window change. */ + public interface WindowFocusChangeListener { + /** + * Notify on focus changed. + * + * @param focusedWindowToken the token of the newly focused window. + */ + void focusChanged(@NonNull IBinder focusedWindowToken); + } + + /** + * Registers a listener to be notified about window focus changes. + * + * @param listener the {@link WindowFocusChangeListener} to register. + */ + public abstract void registerWindowFocusChangeListener(WindowFocusChangeListener listener); + + /** + * Unregisters a listener that was registered via {@link #registerWindowFocusChangeListener}. + * + * @param listener the {@link WindowFocusChangeListener} to unregister. + */ + public abstract void unregisterWindowFocusChangeListener(WindowFocusChangeListener listener); + /** * Interface to receive a callback when the windows reported for * accessibility changed. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 54f960973eff..9fc0339c52a2 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -145,6 +145,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.SHOW_VERBOSE_TRANSA import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.WindowManagerInternal.OnWindowRemovedListener; +import static com.android.server.wm.WindowManagerInternal.WindowFocusChangeListener; import static com.android.server.wm.WindowManagerServiceDumpProto.BACK_NAVIGATION; import static com.android.server.wm.WindowManagerServiceDumpProto.FOCUSED_APP; import static com.android.server.wm.WindowManagerServiceDumpProto.FOCUSED_DISPLAY_ID; @@ -1078,14 +1079,12 @@ public class WindowManagerService extends IWindowManager.Stub private ViewServer mViewServer; final ArrayList<WindowChangeListener> mWindowChangeListeners = new ArrayList<>(); + final ArrayList<WindowFocusChangeListener> mWindowFocusChangeListeners = new ArrayList<>(); boolean mWindowsChanged = false; - public interface WindowChangeListener { + interface WindowChangeListener { /** Notify on windows changed */ void windowsChanged(); - - /** Notify on focus changed */ - void focusChanged(); } final HighRefreshRateDenylist mHighRefreshRateDenylist; @@ -5306,18 +5305,30 @@ public class WindowManagerService extends IWindowManager.Stub return success; } - public void addWindowChangeListener(WindowChangeListener listener) { + void addWindowChangeListener(WindowChangeListener listener) { synchronized (mGlobalLock) { mWindowChangeListeners.add(listener); } } - public void removeWindowChangeListener(WindowChangeListener listener) { + void removeWindowChangeListener(WindowChangeListener listener) { synchronized (mGlobalLock) { mWindowChangeListeners.remove(listener); } } + void addWindowFocusChangeListener(WindowFocusChangeListener listener) { + synchronized (mGlobalLock) { + mWindowFocusChangeListeners.add(listener); + } + } + + void removeWindowFocusChangeListener(WindowFocusChangeListener listener) { + synchronized (mGlobalLock) { + mWindowFocusChangeListeners.remove(listener); + } + } + private void notifyWindowRemovedListeners(IBinder client) { OnWindowRemovedListener[] windowRemovedListeners; synchronized (mGlobalLock) { @@ -5350,18 +5361,19 @@ public class WindowManagerService extends IWindowManager.Stub } } - private void notifyFocusChanged() { - WindowChangeListener[] windowChangeListeners; + private void notifyFocusChanged(IBinder focusedWindowToken) { + WindowFocusChangeListener[] windowFocusChangeListeners; synchronized (mGlobalLock) { - if(mWindowChangeListeners.isEmpty()) { + if(mWindowFocusChangeListeners.isEmpty()) { return; } - windowChangeListeners = new WindowChangeListener[mWindowChangeListeners.size()]; - windowChangeListeners = mWindowChangeListeners.toArray(windowChangeListeners); + windowFocusChangeListeners = + new WindowFocusChangeListener[mWindowFocusChangeListeners.size()]; + mWindowFocusChangeListeners.toArray(windowFocusChangeListeners); } - int N = windowChangeListeners.length; + int N = windowFocusChangeListeners.length; for(int i = 0; i < N; i++) { - windowChangeListeners[i].focusChanged(); + windowFocusChangeListeners[i].focusChanged(focusedWindowToken); } } @@ -5636,7 +5648,7 @@ public class WindowManagerService extends IWindowManager.Stub if (newFocusedWindow != null && newFocusedWindow.mInputChannelToken == newToken) { mAnrController.onFocusChanged(newFocusedWindow); newFocusedWindow.reportFocusChangedSerialized(true); - notifyFocusChanged(); + notifyFocusChanged(newTarget.getWindowToken()); } WindowState lastFocusedWindow = lastTarget != null ? lastTarget.getWindowState() : null; @@ -8650,6 +8662,16 @@ public class WindowManagerService extends IWindowManager.Stub } @Override + public void registerWindowFocusChangeListener(WindowFocusChangeListener listener) { + WindowManagerService.this.addWindowFocusChangeListener(listener); + } + + @Override + public void unregisterWindowFocusChangeListener(WindowFocusChangeListener listener) { + WindowManagerService.this.removeWindowFocusChangeListener(listener); + } + + @Override public void registerOnWindowRemovedListener(OnWindowRemovedListener listener) { synchronized (mGlobalLock) { mOnWindowRemovedListeners.add(listener); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 3b7d31274326..1022d18ac0e9 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5447,7 +5447,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override void assignLayer(Transaction t, int layer) { if (mStartingData != null) { - if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + if (mSyncState != SYNC_STATE_NONE) { // When this container needs to be synced, assign layer with its own sync // transaction to avoid out of ordering when merge. // Still use the passed-in transaction for non-sync case, such as building finish diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 1d8d867f8dcb..0d434f51e082 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -561,7 +561,7 @@ class WindowStateAnimator { break; } if (attr >= 0) { - a = mWin.getDisplayContent().mAppTransition.loadAnimationAttr( + a = mWin.mDisplayContent.mTransitionAnimation.loadAnimationAttr( mWin.mAttrs, attr, TRANSIT_OLD_NONE); } } diff --git a/services/proguard.flags b/services/proguard.flags index 8d8b418ced0b..dd3757c9e360 100644 --- a/services/proguard.flags +++ b/services/proguard.flags @@ -59,6 +59,7 @@ # Referenced in wear-service -keep public class com.android.server.wm.WindowManagerInternal { *; } +-keep public class com.android.server.wm.WindowManagerInternal$WindowFocusChangeListener { *; } # JNI keep rules # The global keep rule for native methods allows stripping of such methods if they're unreferenced diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml index d6a685378d9e..ea01fc4e6140 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidTest.xml @@ -18,9 +18,6 @@ <option name="test-suite-tag" value="apct" /> <option name="test-suite-tag" value="apct-instrumentation" /> - <!-- Needed for reading the app files for the test artifacts --> - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> @@ -44,7 +41,7 @@ <!-- Collect output of DumpOnFailure --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> - <option name="directory-keys" value="/data/user/0/com.android.apps.inputmethod.simpleime/files" /> + <option name="directory-keys" value="/sdcard/DumpOnFailure" /> <option name="collect-on-run-ended-only" value="true" /> <option name="clean-up" value="true" /> </metrics_collector> diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 2cd860ae6c12..e263e85f020f 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -77,6 +77,8 @@ import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; import java.util.concurrent.CountDownLatch; @@ -1294,6 +1296,14 @@ public class InputMethodServiceTest { mInstrumentation.waitForIdleSync(); final var postScreenshot = mInstrumentation.getUiAutomation().takeScreenshot(); mDumpOnFailure.dumpOnFailure("post-getUiObject", postScreenshot); + try { + final var outputStream = new ByteArrayOutputStream(); + mUiDevice.dumpWindowHierarchy(outputStream); + final String windowHierarchy = outputStream.toString(StandardCharsets.UTF_8); + mDumpOnFailure.dumpOnFailure("post-getUiObject", windowHierarchy); + } catch (Exception e) { + Log.i(TAG, "Failed to dump windowHierarchy", e); + } assertWithMessage("UiObject with " + bySelector + " was found").that(uiObject).isNotNull(); return uiObject; } diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml index 00873de4aaed..b6965a4341d7 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml @@ -20,6 +20,9 @@ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <!-- Enable writing output of DumpOnFailure to external storage --> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> + <application android:debuggable="true" android:label="@string/app_name"> <service diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index c151732cec66..65585d06ff06 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -332,6 +332,10 @@ public class DisplayManagerServiceTest { @Override public void destroyDisplay(IBinder displayToken) { } + + @Override + public void setDisplayPowerMode(IBinder displayToken, int mode) { + } }, flags); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java index 9287b3004279..0bef3b89547f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java @@ -21,19 +21,29 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.hardware.display.DisplayManager; import android.hardware.display.IVirtualDisplayCallback; import android.hardware.display.VirtualDisplayConfig; import android.media.projection.IMediaProjection; +import android.os.Binder; import android.os.IBinder; import android.os.PowerManager; import android.os.Process; +import android.platform.test.annotations.EnableFlags; 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.view.Display; import android.view.Surface; +import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -67,6 +77,9 @@ public class VirtualDisplayAdapterTest { public final TestableContext mContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getContext()); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private VirtualDisplayAdapter.SurfaceControlDisplayFactory mMockSufaceControlDisplayFactory; @@ -380,6 +393,69 @@ public class VirtualDisplayAdapterTest { } } + @EnableFlags( + android.companion.virtualdevice.flags.Flags.FLAG_CORRECT_VIRTUAL_DISPLAY_POWER_STATE) + @Test + public void neverBlankDisplay_alwaysOn() { + // A non-public non-mirror display is considered never blank. + DisplayDevice device = mAdapter.createVirtualDisplayLocked(mMockCallback, + /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage", + "uniqueId", /* surface= */ mSurfaceMock, /* flags= */ 0, + mVirtualDisplayConfigMock); + + DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); + assertThat(info.state).isEqualTo(Display.STATE_ON); + assertThat(info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) + .isEqualTo(DisplayDeviceInfo.FLAG_NEVER_BLANK); + } + + @EnableFlags( + android.companion.virtualdevice.flags.Flags.FLAG_CORRECT_VIRTUAL_DISPLAY_POWER_STATE) + @Test + public void virtualDisplayStateChange_propagatesToSurfaceControl() throws Exception { + final String uniqueId = "uniqueId"; + final IBinder displayToken = new Binder(); + when(mMockSufaceControlDisplayFactory.createDisplay( + any(), anyBoolean(), eq(uniqueId), anyFloat())) + .thenReturn(displayToken); + + // The display needs to be public, otherwise it will be considered never blank. + DisplayDevice device = mAdapter.createVirtualDisplayLocked(mMockCallback, + /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage", + uniqueId, /* surface= */ mSurfaceMock, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, + mVirtualDisplayConfigMock); + + DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); + assertThat(info.state).isEqualTo(Display.STATE_UNKNOWN); + assertThat(info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK).isEqualTo(0); + + // Any initial state change is processed because the display state is initially UNKNOWN + Runnable stateOnRunnable = device.requestDisplayStateLocked( + Display.STATE_ON, /* brightnessState= */ 1.0f, /* sdrBrightnessState= */ 1.0f, + /* displayOffloadSession= */ null); + assertThat(stateOnRunnable).isNotNull(); + stateOnRunnable.run(); + verify(mMockSufaceControlDisplayFactory) + .setDisplayPowerMode(displayToken, SurfaceControl.POWER_MODE_NORMAL); + verify(mMockCallback).onResumed(); + + // Requesting the same display state is a no-op + Runnable stateOnSecondRunnable = device.requestDisplayStateLocked( + Display.STATE_ON, /* brightnessState= */ 1.0f, /* sdrBrightnessState= */ 1.0f, + /* displayOffloadSession= */ null); + assertThat(stateOnSecondRunnable).isNull(); + + // A change to the display state is processed + Runnable stateOffRunnable = device.requestDisplayStateLocked( + Display.STATE_OFF, /* brightnessState= */ 1.0f, /* sdrBrightnessState= */ 1.0f, + /* displayOffloadSession= */ null); + assertThat(stateOffRunnable).isNotNull(); + stateOffRunnable.run(); + verify(mMockSufaceControlDisplayFactory) + .setDisplayPowerMode(displayToken, SurfaceControl.POWER_MODE_OFF); + verify(mMockCallback).onPaused(); + } + private IVirtualDisplayCallback createCallback() { return new IVirtualDisplayCallback.Stub() { diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt index 6929690baaf8..d7c047768c1e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt @@ -17,7 +17,6 @@ package com.android.server.display.brightness.clamper import android.os.UserHandle import android.platform.test.annotations.RequiresFlagsEnabled -import android.provider.Settings import android.testing.TestableContext import androidx.test.platform.app.InstrumentationRegistry import com.android.server.display.DisplayDeviceConfig @@ -46,8 +45,6 @@ class BrightnessLowLuxModifierTest { private var mockDisplayDeviceConfig = mock<DisplayDeviceConfig>() private val LOW_LUX_BRIGHTNESS = 0.1f - private val TRANSITION_POINT = 0.25f - private val NORMAL_RANGE_BRIGHTNESS = 0.3f @Before fun setUp() { @@ -73,127 +70,21 @@ class BrightnessLowLuxModifierTest { whenever(mockDisplayDeviceConfig.getBrightnessFromBacklight(/* backlight = */ 0.15f)) .thenReturn(0.24f) - // values above transition point (normal range) - // nits: 10 -> backlight 0.2 -> brightness -> 0.3 - whenever(mockDisplayDeviceConfig.getBacklightFromNits(/* nits= */ 10f)) - .thenReturn(0.2f) - whenever(mockDisplayDeviceConfig.getBrightnessFromBacklight(/* backlight = */ 0.2f)) - .thenReturn(NORMAL_RANGE_BRIGHTNESS) - // min nits when lux of 400 whenever(mockDisplayDeviceConfig.getMinNitsFromLux(/* lux= */ 400f)) .thenReturn(1.0f) - - whenever(mockDisplayDeviceConfig.evenDimmerTransitionPoint).thenReturn(TRANSITION_POINT) - - testHandler.flush() - } - - @Test - fun testSettingOffDisablesModifier() { - // test transition point ensures brightness doesn't drop when setting is off. - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, USER_ID) - modifier.recalculateLowerBound() - testHandler.flush() - assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) - assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off - modifier.setAmbientLux(3000f) - testHandler.flush() - assertThat(modifier.isActive).isFalse() - assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) - assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off } @Test @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) fun testLuxRestrictsBrightnessRange() { // test that high lux prevents low brightness range. - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, USER_ID) - Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.1f, USER_ID) - modifier.setAmbientLux(400f) - - testHandler.flush() - - assertThat(modifier.isActive).isTrue() - // Test restriction from lux setting - assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) - assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS) - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) - fun testUserRestrictsBrightnessRange() { - // test that user minimum nits setting prevents low brightness range. - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, USER_ID) - Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 10.0f, USER_ID) - modifier.recalculateLowerBound() - testHandler.flush() - - // Test restriction from user setting - assertThat(modifier.isActive).isTrue() - assertThat(modifier.brightnessReason) - .isEqualTo(BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND) - assertThat(modifier.brightnessLowerBound).isEqualTo(NORMAL_RANGE_BRIGHTNESS) - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) - fun testOnToOff() { - // test that high lux prevents low brightness range. - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, USER_ID) // on - Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, USER_ID) - modifier.setAmbientLux(400f) - - testHandler.flush() - - assertThat(modifier.isActive).isTrue() - // Test restriction from lux setting - assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) - assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS) - - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, USER_ID) // off - - modifier.recalculateLowerBound() - testHandler.flush() - - assertThat(modifier.isActive).isFalse() - assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) - assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) - fun testOffToOn() { - // test that high lux prevents low brightness range. - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, USER_ID) // off - Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, USER_ID) modifier.setAmbientLux(400f) testHandler.flush() - assertThat(modifier.isActive).isFalse() - assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) - assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off - - - - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, USER_ID) // on - modifier.recalculateLowerBound() - testHandler.flush() - assertThat(modifier.isActive).isTrue() // Test restriction from lux setting assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) @@ -204,11 +95,6 @@ class BrightnessLowLuxModifierTest { @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) fun testEnabledEvenWhenAutobrightnessIsOff() { // test that high lux prevents low brightness range. - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, USER_ID) // on - Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, USER_ID) - modifier.setAmbientLux(400f) testHandler.flush() @@ -225,37 +111,5 @@ class BrightnessLowLuxModifierTest { assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS) } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) - fun testUserSwitch() { - // nits: 0.5 -> backlight 0.01 -> brightness -> 0.05 - whenever(mockDisplayDeviceConfig.getBacklightFromNits(/* nits= */ 0.5f)) - .thenReturn(0.01f) - whenever(mockDisplayDeviceConfig.getBrightnessFromBacklight(/* backlight = */ 0.01f)) - .thenReturn(0.05f) - - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, USER_ID) // off - Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, USER_ID) - - modifier.recalculateLowerBound() - - assertThat(modifier.isActive).isFalse() - assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) - assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - i.e. off - - Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, USER_ID) // on - Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.5f, USER_ID) - modifier.onSwitchUser() - - assertThat(modifier.isActive).isTrue() - assertThat(modifier.brightnessReason).isEqualTo( - BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND) - assertThat(modifier.brightnessLowerBound).isEqualTo(0.05f) - } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java index 22c10f967398..067230a66228 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/color/ColorDisplayServiceTest.java @@ -29,7 +29,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.app.ActivityManager; @@ -113,10 +112,10 @@ public class ColorDisplayServiceTest { doReturn(mContext).when(mContext).getApplicationContext(); final Resources res = Mockito.spy(mContext.getResources()); + doReturn(res).when(mContext).getResources(); doReturn(MINIMAL_COLOR_MODES).when(res).getIntArray(R.array.config_availableColorModes); doReturn(true).when(res).getBoolean(R.bool.config_nightDisplayAvailable); doReturn(true).when(res).getBoolean(R.bool.config_displayWhiteBalanceAvailable); - when(mContext.getResources()).thenReturn(res); mResourcesSpy = res; mUserId = ActivityManager.getCurrentUser(); @@ -1105,10 +1104,11 @@ public class ColorDisplayServiceTest { @Test public void compositionColorSpaces_noResources() { - when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes)) - .thenReturn(new int[] {}); - when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces)) - .thenReturn(new int[] {}); + doReturn(new int[] {}).when(mResourcesSpy) + .getIntArray(R.array.config_displayCompositionColorModes); + doReturn(new int[] {}).when(mResourcesSpy) + .getIntArray(R.array.config_displayCompositionColorSpaces); + setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL); startService(); verify(mDisplayTransformManager).setColorMode( @@ -1118,16 +1118,15 @@ public class ColorDisplayServiceTest { @Test public void compositionColorSpaces_invalidResources() { - when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes)) - .thenReturn(new int[] { - ColorDisplayManager.COLOR_MODE_NATURAL, - // Missing second color mode - }); - when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces)) - .thenReturn(new int[] { - Display.COLOR_MODE_SRGB, - Display.COLOR_MODE_DISPLAY_P3 - }); + doReturn(new int[] { + ColorDisplayManager.COLOR_MODE_NATURAL, + // Missing second color mode + }).when(mResourcesSpy).getIntArray(R.array.config_displayCompositionColorModes); + doReturn(new int[] { + Display.COLOR_MODE_SRGB, + Display.COLOR_MODE_DISPLAY_P3 + }).when(mResourcesSpy).getIntArray(R.array.config_displayCompositionColorSpaces); + setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL); startService(); verify(mDisplayTransformManager).setColorMode( @@ -1137,14 +1136,13 @@ public class ColorDisplayServiceTest { @Test public void compositionColorSpaces_validResources_validColorMode() { - when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes)) - .thenReturn(new int[] { - ColorDisplayManager.COLOR_MODE_NATURAL - }); - when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces)) - .thenReturn(new int[] { - Display.COLOR_MODE_SRGB, - }); + doReturn(new int[] { + ColorDisplayManager.COLOR_MODE_NATURAL + }).when(mResourcesSpy).getIntArray(R.array.config_displayCompositionColorModes); + doReturn(new int[] { + Display.COLOR_MODE_SRGB, + }).when(mResourcesSpy).getIntArray(R.array.config_displayCompositionColorSpaces); + setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL); startService(); verify(mDisplayTransformManager).setColorMode( @@ -1154,14 +1152,13 @@ public class ColorDisplayServiceTest { @Test public void compositionColorSpaces_validResources_invalidColorMode() { - when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorModes)) - .thenReturn(new int[] { - ColorDisplayManager.COLOR_MODE_NATURAL - }); - when(mResourcesSpy.getIntArray(R.array.config_displayCompositionColorSpaces)) - .thenReturn(new int[] { - Display.COLOR_MODE_SRGB, - }); + doReturn(new int[] { + ColorDisplayManager.COLOR_MODE_NATURAL + }).when(mResourcesSpy).getIntArray(R.array.config_displayCompositionColorModes); + doReturn(new int[] { + Display.COLOR_MODE_SRGB, + }).when(mResourcesSpy).getIntArray(R.array.config_displayCompositionColorSpaces); + setColorMode(ColorDisplayManager.COLOR_MODE_BOOSTED); startService(); verify(mDisplayTransformManager).setColorMode( @@ -1171,8 +1168,7 @@ public class ColorDisplayServiceTest { @Test public void getColorMode_noAvailableModes_returnsNotSet() { - when(mResourcesSpy.getIntArray(R.array.config_availableColorModes)) - .thenReturn(new int[] {}); + doReturn(new int[] {}).when(mResourcesSpy).getIntArray(R.array.config_availableColorModes); startService(); verify(mDisplayTransformManager, never()).setColorMode(anyInt(), any(), any(), anyInt()); assertThat(mBinderService.getColorMode()).isEqualTo(-1); @@ -1197,16 +1193,17 @@ public class ColorDisplayServiceTest { @Test public void sliderScalesWithinRange() { + doReturn(true).when(mRbcSpy).isAvailable(mContext); + + doReturn(85).when(mResourcesSpy).getInteger( + R.integer.config_reduceBrightColorsStrengthMax); + doReturn(10).when(mResourcesSpy).getInteger( + R.integer.config_reduceBrightColorsStrengthMin); + doReturn(44).when(mResourcesSpy).getInteger( + R.integer.config_reduceBrightColorsStrengthDefault); + // setup startService(); - reset(mRbcSpy); - doReturn(true).when(mRbcSpy).isAvailable(mContext); - when(mContext.getResources().getInteger( - R.integer.config_reduceBrightColorsStrengthMax)).thenReturn(85); - when(mContext.getResources().getInteger( - R.integer.config_reduceBrightColorsStrengthMin)).thenReturn(10); - when(mContext.getResources().getInteger( - R.integer.config_reduceBrightColorsStrengthDefault)).thenReturn(44); // Valid value test // // set on, and to 90% of range diff --git a/services/tests/media/mediarouterservicetest/Android.bp b/services/tests/media/mediarouterservicetest/Android.bp index aed3af6b69f6..f149f2ec8d56 100644 --- a/services/tests/media/mediarouterservicetest/Android.bp +++ b/services/tests/media/mediarouterservicetest/Android.bp @@ -9,6 +9,10 @@ package { android_test { name: "MediaRouterServiceTests", + defaults: [ + // For ExtendedMockito dependencies. + "modules-utils-testable-device-config-defaults", + ], srcs: [ "src/**/*.java", ], @@ -23,12 +27,17 @@ android_test { "services.core", "truth", ], + libs: [ + "android.test.base.stubs", + "android.test.runner.stubs", + ], platform_apis: true, test_suites: [ // "device-tests", "general-tests", + "mts-statsd", ], certificate: "platform", @@ -36,4 +45,5 @@ android_test { optimize: { enabled: false, }, + min_sdk_version: "30", } diff --git a/services/tests/media/mediarouterservicetest/AndroidTest.xml b/services/tests/media/mediarouterservicetest/AndroidTest.xml index b0656816b701..646812b076d6 100644 --- a/services/tests/media/mediarouterservicetest/AndroidTest.xml +++ b/services/tests/media/mediarouterservicetest/AndroidTest.xml @@ -17,6 +17,7 @@ <option name="test-tag" value="MediaRouterServiceTests" /> <option name="test-suite-tag" value="apct" /> <option name="test-suite-tag" value="apct-instrumentation" /> + <option name="test-suite-tag" value="mts" /> <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> @@ -26,7 +27,7 @@ <option name="install-arg" value="-t" /> </target_preparer> - <test class="com.android.tradefed.testtype.InstrumentationTest" > + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="com.android.server.media.tests" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> <option name="hidden-api-checks" value="false"/> diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/MediaRouterMetricLoggerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/MediaRouterMetricLoggerTest.java new file mode 100644 index 000000000000..5e401aefc24c --- /dev/null +++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/MediaRouterMetricLoggerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 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; + +import static android.media.MediaRoute2ProviderService.REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA; +import static android.media.MediaRoute2ProviderService.REASON_INVALID_COMMAND; +import static android.media.MediaRoute2ProviderService.REASON_NETWORK_ERROR; +import static android.media.MediaRoute2ProviderService.REASON_REJECTED; +import static android.media.MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE; +import static android.media.MediaRoute2ProviderService.REASON_UNIMPLEMENTED; +import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_FAILED_TO_REROUTE_SYSTEM_MEDIA; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_COMMAND; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_NETWORK_ERROR; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_REJECTED; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTE_NOT_AVAILABLE; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_SUCCESS; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNIMPLEMENTED; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNKNOWN_ERROR; +import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED; +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; +import com.android.modules.utils.testing.ExtendedMockitoRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class MediaRouterMetricLoggerTest { + @Rule + public final ExtendedMockitoRule mExtendedMockitoRule = + new ExtendedMockitoRule.Builder(this).mockStatic(MediaRouterStatsLog.class).build(); + + private MediaRouterMetricLogger mLogger; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLogger = new MediaRouterMetricLogger(); + } + + @Test + public void addRequestInfo_addsRequestInfoToCache() { + long requestId = 123; + int eventType = MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; + + mLogger.addRequestInfo(requestId, eventType); + + assertThat(mLogger.getRequestCacheSize()).isEqualTo(1); + } + + @Test + public void removeRequestInfo_removesRequestInfoFromCache() { + long requestId = 123; + int eventType = MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; + mLogger.addRequestInfo(requestId, eventType); + + mLogger.removeRequestInfo(requestId); + + assertThat(mLogger.getRequestCacheSize()).isEqualTo(0); + } + + @Test + public void logOperationFailure_logsOperationFailure() { + int eventType = MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; + int result = MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_REJECTED; + mLogger.logOperationFailure(eventType, result); + verify( + () -> + MediaRouterStatsLog.write( // Use ExtendedMockito.verify and lambda + MEDIA_ROUTER_EVENT_REPORTED, eventType, result)); + } + + @Test + public void logRequestResult_logsRequestResult() { + long requestId = 123; + int eventType = MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION; + int result = MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_SUCCESS; + mLogger.addRequestInfo(requestId, eventType); + + mLogger.logRequestResult(requestId, result); + + assertThat(mLogger.getRequestCacheSize()).isEqualTo(0); + verify( + () -> + MediaRouterStatsLog.write( // Use ExtendedMockito.verify and lambda + MEDIA_ROUTER_EVENT_REPORTED, eventType, result)); + } + + @Test + public void convertResultFromReason_returnsCorrectResult() { + assertThat(MediaRouterMetricLogger.convertResultFromReason(REASON_UNKNOWN_ERROR)) + .isEqualTo(MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNKNOWN_ERROR); + assertThat(MediaRouterMetricLogger.convertResultFromReason(REASON_REJECTED)) + .isEqualTo(MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_REJECTED); + assertThat(MediaRouterMetricLogger.convertResultFromReason(REASON_NETWORK_ERROR)) + .isEqualTo(MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_NETWORK_ERROR); + assertThat(MediaRouterMetricLogger.convertResultFromReason(REASON_ROUTE_NOT_AVAILABLE)) + .isEqualTo(MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_ROUTE_NOT_AVAILABLE); + assertThat(MediaRouterMetricLogger.convertResultFromReason(REASON_INVALID_COMMAND)) + .isEqualTo(MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_INVALID_COMMAND); + assertThat(MediaRouterMetricLogger.convertResultFromReason(REASON_UNIMPLEMENTED)) + .isEqualTo(MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNIMPLEMENTED); + assertThat( + MediaRouterMetricLogger.convertResultFromReason( + REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA)) + .isEqualTo( + MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_FAILED_TO_REROUTE_SYSTEM_MEDIA); + assertThat(MediaRouterMetricLogger.convertResultFromReason(-1)) + .isEqualTo(MEDIA_ROUTER_EVENT_REPORTED__RESULT__RESULT_UNSPECIFIED); + } + + @Test + public void getRequestCacheSize_returnsCorrectSize() { + assertThat(mLogger.getRequestCacheSize()).isEqualTo(0); + mLogger.addRequestInfo( + 123, MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION); + assertThat(mLogger.getRequestCacheSize()).isEqualTo(1); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 340115a7d465..5d8f57866f7d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -43,6 +43,7 @@ import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_ACTIVITY; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_NONE; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_SHORT_FGS_TIMEOUT; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -107,6 +108,7 @@ import android.os.PowerManagerInternal; import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; +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; @@ -698,7 +700,7 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test - @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + @EnableFlags({Flags.FLAG_USE_CPU_TIME_CAPABILITY, Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING}) public void testUpdateOomAdjFreezeState_bindingFromShortFgs() { // Setting up a started short FGS within app1. final ServiceRecord s = ServiceRecord.newEmptyInstanceForTest(mService); @@ -744,6 +746,44 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + @DisableFlags(Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING) + public void testUpdateOomAdjFreezeState_bindingFromFgs() { + final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + mProcessStateController.setHasForegroundServices(app.mServices, true, + FOREGROUND_SERVICE_TYPE_SPECIAL_USE, false); + + final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID, + MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false)); + // App with a foreground service binds to app2 + bindService(app2, app, null, null, 0, mock(IBinder.class)); + + setProcessesToLru(app, app2); + updateOomAdj(app); + + assertCpuTime(app); + assertCpuTime(app2); + } + + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + @DisableFlags(Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING) + public void testUpdateOomAdjFreezeState_soloFgs() { + final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); + mProcessStateController.setHasForegroundServices(app.mServices, true, + FOREGROUND_SERVICE_TYPE_SPECIAL_USE, false); + + setProcessesToLru(app); + updateOomAdj(app); + + assertCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) public void testUpdateOomAdjFreezeState_receivers() { final ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java index d6349fc0651f..ab3784b07e10 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/PendingIntentControllerTest.java @@ -32,7 +32,6 @@ import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_SUPERSEDED import static com.android.server.am.PendingIntentRecord.CANCEL_REASON_USER_STOPPED; import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.cancelReasonToString; -import static com.android.window.flags.Flags.balClearAllowlistDuration; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -216,9 +215,7 @@ public class PendingIntentControllerTest { pir.getAllowlistDurationLocked(token); assertNotNull(allowlistDurationLockedAfterClear); assertEquals(1000, allowlistDurationLockedAfterClear.duration); - assertEquals(balClearAllowlistDuration() - ? TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED - : TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, + assertEquals(TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, allowlistDurationLocked.type); } diff --git a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp index e030b3f19e4f..16adf8f8c7fe 100644 --- a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp +++ b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp @@ -60,4 +60,8 @@ android_test { "mts-crashrecovery", ], min_sdk_version: "36", + + // Test coverage system runs on different devices. Need to + // compile for all architecture. + compile_multilib: "both", } diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp b/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp index 36b064b9b090..7c02370ef758 100644 --- a/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp +++ b/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp @@ -58,6 +58,10 @@ android_test { "mts-crashrecovery", ], min_sdk_version: "36", + + // Test coverage system runs on different devices. Need to + // compile for all architecture. + compile_multilib: "both", } test_module_config { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java index f7b16c808c50..dd089fcb1d70 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java @@ -201,11 +201,11 @@ public class AutoclickTypePanelTest { public void moveToNextCorner_positionButton_rotatesThroughAllPositions() { // Define all positions in sequence int[][] expectedPositions = { - {0, Gravity.END | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90}, - {1, Gravity.START | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90}, - {2, Gravity.START | Gravity.TOP, /*x=*/ 15, /*y=*/ 30}, - {3, Gravity.END | Gravity.TOP, /*x=*/ 15, /*y=*/ 30}, - {0, Gravity.END | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90} + {CORNER_BOTTOM_RIGHT, Gravity.END | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90}, + {CORNER_BOTTOM_LEFT, Gravity.START | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90}, + {CORNER_TOP_LEFT, Gravity.START | Gravity.TOP, /*x=*/ 15, /*y=*/ 30}, + {CORNER_TOP_RIGHT, Gravity.END | Gravity.TOP, /*x=*/ 15, /*y=*/ 30}, + {CORNER_BOTTOM_RIGHT, Gravity.END | Gravity.BOTTOM, /*x=*/ 15, /*y=*/ 90} }; // Check initial position @@ -270,7 +270,7 @@ public class AutoclickTypePanelTest { int screenWidth = mTestableContext.getResources().getDisplayMetrics().widthPixels; // Verify initial corner is bottom-right. - assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()) + assertThat(mAutoclickTypePanel.getCurrentCornerForTesting()) .isEqualTo(CORNER_BOTTOM_RIGHT); dispatchDragSequence(contentView, @@ -279,7 +279,7 @@ public class AutoclickTypePanelTest { // Verify snapping to the right. assertThat(params.gravity).isEqualTo(Gravity.END | Gravity.TOP); - assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()) + assertThat(mAutoclickTypePanel.getCurrentCornerForTesting()) .isEqualTo(CORNER_TOP_RIGHT); } @@ -293,7 +293,7 @@ public class AutoclickTypePanelTest { int screenWidth = mTestableContext.getResources().getDisplayMetrics().widthPixels; // Verify initial corner is bottom-right. - assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()) + assertThat(mAutoclickTypePanel.getCurrentCornerForTesting()) .isEqualTo(CORNER_BOTTOM_RIGHT); dispatchDragSequence(contentView, @@ -302,7 +302,7 @@ public class AutoclickTypePanelTest { // Verify snapping to the left. assertThat(params.gravity).isEqualTo(Gravity.START | Gravity.TOP); - assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()) + assertThat(mAutoclickTypePanel.getCurrentCornerForTesting()) .isEqualTo(CORNER_BOTTOM_LEFT); } @@ -319,7 +319,7 @@ public class AutoclickTypePanelTest { // Verify panel is positioned at default bottom-right corner. WindowManager.LayoutParams params = panel.getLayoutParamsForTesting(); - assertThat(panel.getCurrentCornerIndexForTesting()).isEqualTo(CORNER_BOTTOM_RIGHT); + assertThat(panel.getCurrentCornerForTesting()).isEqualTo(CORNER_BOTTOM_RIGHT); assertThat(params.gravity).isEqualTo(Gravity.END | Gravity.BOTTOM); assertThat(params.x).isEqualTo(15); // Default edge margin. assertThat(params.y).isEqualTo(90); // Default bottom offset. @@ -353,7 +353,7 @@ public class AutoclickTypePanelTest { assertThat(params.gravity).isEqualTo(Gravity.START | Gravity.TOP); assertThat(params.x).isEqualTo(15); assertThat(params.y).isEqualTo(30); - assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()).isEqualTo( + assertThat(mAutoclickTypePanel.getCurrentCornerForTesting()).isEqualTo( CORNER_TOP_LEFT); } @@ -392,7 +392,7 @@ public class AutoclickTypePanelTest { assertThat(params.gravity).isEqualTo(Gravity.START | Gravity.TOP); assertThat(params.x).isEqualTo(15); // PANEL_EDGE_MARGIN assertThat(params.y).isEqualTo(panelLocation[1] + 10); - assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()).isEqualTo( + assertThat(mAutoclickTypePanel.getCurrentCornerForTesting()).isEqualTo( CORNER_BOTTOM_LEFT); } @@ -453,7 +453,7 @@ public class AutoclickTypePanelTest { private void verifyPanelPosition(int[] expectedPosition) { WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting(); - assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()).isEqualTo( + assertThat(mAutoclickTypePanel.getCurrentCornerForTesting()).isEqualTo( expectedPosition[0]); assertThat(params.gravity).isEqualTo(expectedPosition[1]); assertThat(params.x).isEqualTo(expectedPosition[2]); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java index ac27a971102a..9ec99c651691 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java @@ -30,7 +30,10 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -248,6 +251,40 @@ public class FullScreenMagnificationControllerTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) + public void testRegister_RegistersPointerMotionFilter() { + register(DISPLAY_0); + + verify(mMockInputManager).registerAccessibilityPointerMotionFilter( + any(InputManagerInternal.AccessibilityPointerMotionFilter.class)); + + // If a filter is already registered, adding a display won't invoke another filter + // registration. + clearInvocations(mMockInputManager); + register(DISPLAY_1); + register(INVALID_DISPLAY); + + verify(mMockInputManager, times(0)).registerAccessibilityPointerMotionFilter( + any(InputManagerInternal.AccessibilityPointerMotionFilter.class)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) + public void testUnregister_UnregistersPointerMotionFilter() { + register(DISPLAY_0); + register(DISPLAY_1); + clearInvocations(mMockInputManager); + + mFullScreenMagnificationController.unregister(DISPLAY_1); + // There's still an active display. Don't unregister yet. + verify(mMockInputManager, times(0)).registerAccessibilityPointerMotionFilter( + nullable(InputManagerInternal.AccessibilityPointerMotionFilter.class)); + + mFullScreenMagnificationController.unregister(DISPLAY_0); + verify(mMockInputManager, times(1)).registerAccessibilityPointerMotionFilter(isNull()); + } + + @Test public void testInitialState_noMagnificationAndMagnificationRegionReadFromWindowManager() { for (int i = 0; i < DISPLAY_COUNT; i++) { initialState_noMagnificationAndMagnificationRegionReadFromWindowManager(i); @@ -699,6 +736,63 @@ public class FullScreenMagnificationControllerTest { } @Test + public void testSetOffset_whileMagnifying_offsetsMove() { + for (int i = 0; i < DISPLAY_COUNT; i++) { + setOffset_whileMagnifying_offsetsMove(i); + resetMockWindowManager(); + } + } + + private void setOffset_whileMagnifying_offsetsMove(int displayId) { + register(displayId); + PointF startCenter = INITIAL_MAGNIFICATION_BOUNDS_CENTER; + for (final float scale : new float[]{2.0f, 2.5f, 3.0f}) { + assertTrue(mFullScreenMagnificationController + .setScaleAndCenter(displayId, scale, startCenter.x, startCenter.y, true, false, + SERVICE_ID_1)); + mMessageCapturingHandler.sendAllMessages(); + + for (final PointF center : new PointF[]{ + INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER, + INITIAL_BOUNDS_UPPER_LEFT_2X_CENTER}) { + Mockito.clearInvocations(mMockWindowManager); + PointF newOffsets = computeOffsets(INITIAL_MAGNIFICATION_BOUNDS, center, scale); + mFullScreenMagnificationController.setOffset(displayId, newOffsets.x, newOffsets.y, + SERVICE_ID_1); + mMessageCapturingHandler.sendAllMessages(); + + MagnificationSpec expectedSpec = getMagnificationSpec(scale, newOffsets); + verify(mMockWindowManager) + .setMagnificationSpec(eq(displayId), argThat(closeTo(expectedSpec))); + assertEquals(center.x, mFullScreenMagnificationController.getCenterX(displayId), + 0.0); + assertEquals(center.y, mFullScreenMagnificationController.getCenterY(displayId), + 0.0); + verify(mMockValueAnimator, times(0)).start(); + } + } + } + + @Test + public void testSetOffset_whileNotMagnifying_hasNoEffect() { + for (int i = 0; i < DISPLAY_COUNT; i++) { + setOffset_whileNotMagnifying_hasNoEffect(i); + resetMockWindowManager(); + } + } + + private void setOffset_whileNotMagnifying_hasNoEffect(int displayId) { + register(displayId); + Mockito.reset(mMockWindowManager); + MagnificationSpec startSpec = getCurrentMagnificationSpec(displayId); + mFullScreenMagnificationController.setOffset(displayId, 100, 100, SERVICE_ID_1); + assertThat(getCurrentMagnificationSpec(displayId), closeTo(startSpec)); + mFullScreenMagnificationController.setOffset(displayId, 200, 200, SERVICE_ID_1); + assertThat(getCurrentMagnificationSpec(displayId), closeTo(startSpec)); + verifyNoMoreInteractions(mMockWindowManager); + } + + @Test @RequiresFlagsEnabled(Flags.FLAG_FULLSCREEN_FLING_GESTURE) public void testStartFling_whileMagnifying_flings() throws InterruptedException { for (int i = 0; i < DISPLAY_COUNT; i++) { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index 5c126d1f5d3f..4ea5fcfd79c8 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -1419,6 +1419,12 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) + public void testMouseMoveEventsDoNotMoveMagnifierViewport() { + runMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE); + } + + @Test public void testStylusMoveEventsDoNotMoveMagnifierViewport() { runMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_STYLUS); } @@ -1467,11 +1473,28 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) + public void testMouseHoverMoveEventsDoNotMoveMagnifierViewport() { + // Note that this means mouse hover shouldn't be handled here. + // FullScreenMagnificationPointerMotionEventFilter handles mouse input events. + runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) + public void testStylusHoverMoveEventsDoNotMoveMagnifierViewport() { + // TODO(b/398984690): We will revisit the behavior. + runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_STYLUS); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) public void testMouseHoverMoveEventsMoveMagnifierViewport() { runHoverMovesViewportTest(InputDevice.SOURCE_MOUSE); } @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) public void testStylusHoverMoveEventsMoveMagnifierViewport() { runHoverMovesViewportTest(InputDevice.SOURCE_STYLUS); } @@ -1497,6 +1520,7 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER) public void testMouseMoveEventsMoveMagnifierViewport() { final EventCaptor eventCaptor = new EventCaptor(); mMgh.setNext(eventCaptor); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationPointerMotionEventFilterTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationPointerMotionEventFilterTest.java new file mode 100644 index 000000000000..a8315d4eb3a5 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationPointerMotionEventFilterTest.java @@ -0,0 +1,79 @@ +/* + * 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.accessibility.magnification; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class FullScreenMagnificationPointerMotionEventFilterTest { + @Mock + private FullScreenMagnificationController mMockFullScreenMagnificationController; + + private FullScreenMagnificationPointerMotionEventFilter mFilter; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mFilter = new FullScreenMagnificationPointerMotionEventFilter( + mMockFullScreenMagnificationController); + } + + @Test + public void inactiveDisplay_doNothing() { + when(mMockFullScreenMagnificationController.isActivated(anyInt())).thenReturn(false); + + float[] delta = new float[]{1.f, 2.f}; + float[] result = mFilter.filterPointerMotionEvent(delta[0], delta[1], 3.0f, 4.0f, 0); + assertThat(result).isEqualTo(delta); + } + + @Test + public void testContinuousMove() { + when(mMockFullScreenMagnificationController.isActivated(anyInt())).thenReturn(true); + when(mMockFullScreenMagnificationController.getScale(anyInt())).thenReturn(3.f); + + float[] delta = new float[]{5.f, 10.f}; + float[] result = mFilter.filterPointerMotionEvent(delta[0], delta[1], 20.f, 30.f, 0); + assertThat(result).isEqualTo(delta); + // At the first cursor move, it goes to (20, 30) + (5, 10) = (25, 40). The scale is 3.0. + // The expected offset is (-25 * (3-1), -40 * (3-1)) = (-50, -80). + verify(mMockFullScreenMagnificationController) + .setOffset(eq(0), eq(-50.f), eq(-80.f), anyInt()); + + float[] delta2 = new float[]{10.f, 5.f}; + float[] result2 = mFilter.filterPointerMotionEvent(delta2[0], delta2[1], 25.f, 40.f, 0); + assertThat(result2).isEqualTo(delta2); + // At the second cursor move, it goes to (25, 40) + (10, 5) = (35, 40). The scale is 3.0. + // The expected offset is (-35 * (3-1), -45 * (3-1)) = (-70, -90). + verify(mMockFullScreenMagnificationController) + .setOffset(eq(0), eq(-70.f), eq(-90.f), anyInt()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java index 1de864cb4eb0..565a9b6c1c44 100644 --- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java @@ -17,6 +17,7 @@ package com.android.server.location.contexthub; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.timeout; @@ -42,12 +43,10 @@ import android.os.Binder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.util.Log; + import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import java.util.Collections; -import java.util.List; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -57,6 +56,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Collections; +import java.util.List; + @RunWith(AndroidJUnit4.class) @Presubmit public class ContextHubEndpointTest { @@ -73,6 +75,12 @@ public class ContextHubEndpointTest { private static final String TARGET_ENDPOINT_NAME = "Example target endpoint"; private static final int TARGET_ENDPOINT_ID = 1; + private static final int SAMPLE_MESSAGE_TYPE = 1234; + private static final HubMessage SAMPLE_MESSAGE = + new HubMessage.Builder(SAMPLE_MESSAGE_TYPE, new byte[] {1, 2, 3, 4, 5}) + .setResponseRequired(true) + .build(); + private ContextHubClientManager mClientManager; private ContextHubEndpointManager mEndpointManager; private HubInfoRegistry mHubInfoRegistry; @@ -229,23 +237,34 @@ public class ContextHubEndpointTest { assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0); } + @Test + public void testDuplicateMessageRejected() throws RemoteException { + IContextHubEndpoint endpoint = registerExampleEndpoint(); + int sessionId = openTestSession(endpoint); + + mEndpointManager.onMessageReceived(sessionId, SAMPLE_MESSAGE); + ArgumentCaptor<HubMessage> messageCaptor = ArgumentCaptor.forClass(HubMessage.class); + verify(mMockCallback).onMessageReceived(eq(sessionId), messageCaptor.capture()); + assertThat(messageCaptor.getValue()).isEqualTo(SAMPLE_MESSAGE); + + // Send a duplicate message and confirm it can be rejected + mEndpointManager.onMessageReceived(sessionId, SAMPLE_MESSAGE); + ArgumentCaptor<MessageDeliveryStatus> statusCaptor = + ArgumentCaptor.forClass(MessageDeliveryStatus.class); + verify(mMockEndpointCommunications) + .sendMessageDeliveryStatusToEndpoint(eq(sessionId), statusCaptor.capture()); + assertThat(statusCaptor.getValue().messageSequenceNumber) + .isEqualTo(SAMPLE_MESSAGE.getMessageSequenceNumber()); + assertThat(statusCaptor.getValue().errorCode).isEqualTo(ErrorCode.TRANSIENT_ERROR); + + unregisterExampleEndpoint(endpoint); + } + /** A helper method to create a session and validates reliable message sending. */ private void testMessageTransactionInternal( IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException { - HubEndpointInfo targetInfo = - new HubEndpointInfo( - TARGET_ENDPOINT_NAME, - TARGET_ENDPOINT_ID, - ENDPOINT_PACKAGE_NAME, - Collections.emptyList()); - int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null); - mEndpointManager.onEndpointSessionOpenComplete(sessionId); + int sessionId = openTestSession(endpoint); - final int messageType = 1234; - HubMessage message = - new HubMessage.Builder(messageType, new byte[] {1, 2, 3, 4, 5}) - .setResponseRequired(true) - .build(); IContextHubTransactionCallback callback = new IContextHubTransactionCallback.Stub() { @Override @@ -258,13 +277,13 @@ public class ContextHubEndpointTest { Log.i(TAG, "Received onTransactionComplete callback, result=" + result); } }; - endpoint.sendMessage(sessionId, message, callback); + endpoint.sendMessage(sessionId, SAMPLE_MESSAGE, callback); ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); verify(mMockEndpointCommunications, timeout(1000)) .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture()); Message halMessage = messageCaptor.getValue(); - assertThat(halMessage.type).isEqualTo(message.getMessageType()); - assertThat(halMessage.content).isEqualTo(message.getMessageBody()); + assertThat(halMessage.type).isEqualTo(SAMPLE_MESSAGE.getMessageType()); + assertThat(halMessage.content).isEqualTo(SAMPLE_MESSAGE.getMessageBody()); assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(1); if (deliverMessageStatus) { @@ -308,4 +327,16 @@ public class ContextHubEndpointTest { .isEqualTo(expectedInfo.getIdentifier().getHub()); assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0); } + + private int openTestSession(IContextHubEndpoint endpoint) throws RemoteException { + HubEndpointInfo targetInfo = + new HubEndpointInfo( + TARGET_ENDPOINT_NAME, + TARGET_ENDPOINT_ID, + ENDPOINT_PACKAGE_NAME, + Collections.emptyList()); + int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null); + mEndpointManager.onEndpointSessionOpenComplete(sessionId); + return sessionId; + } } 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 3727bbefb1d3..dd5c601619a0 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -5211,41 +5211,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void - updateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm() - throws Exception { - mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(mPkg, mUserId)) - .thenReturn(singletonList(mock(AssociationInfo.class))); - when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(), - eq(mTestNotificationChannel.getId()), anyBoolean())) - .thenReturn(mTestNotificationChannel); - - // Missing Uri permissions for the old channel sound - final Uri oldSoundUri = Settings.System.DEFAULT_NOTIFICATION_URI; - doThrow(new SecurityException("no access")).when(mUgmInternal) - .checkGrantUriPermission(eq(Process.myUid()), any(), eq(oldSoundUri), - anyInt(), eq(Process.myUserHandle().getIdentifier())); - - // Has Uri permissions for the old channel sound - final Uri newSoundUri = Uri.parse("content://media/test/sound/uri"); - final NotificationChannel updatedNotificationChannel = new NotificationChannel( - TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); - updatedNotificationChannel.setSound(newSoundUri, - updatedNotificationChannel.getAudioAttributes()); - - mBinderService.updateNotificationChannelFromPrivilegedListener( - null, mPkg, Process.myUserHandle(), updatedNotificationChannel); - - verify(mPreferencesHelper, times(1)).updateNotificationChannel( - anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); - - verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg), - eq(Process.myUserHandle()), eq(mTestNotificationChannel), - eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); - } - - @Test public void testGetNotificationChannelFromPrivilegedListener_cdm_success() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); when(mCompanionMgr.getAssociations(mPkg, mUserId)) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index 4391152220c0..e9cf036d0fb3 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -1009,6 +1009,65 @@ public class NotificationRecordTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) + public void testSummarization_null() { + StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, + true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, + false /* lights */, false /* defaultLights */, groupId /* group */); + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); + + assertThat(record.getSummarization()).isNull(); + + Bundle signals = new Bundle(); + signals.putCharSequence(Adjustment.KEY_SUMMARIZATION, null); + record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId())); + + record.applyAdjustments(); + + assertThat(record.getSummarization()).isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) + public void testSummarization_charSequence() { + CharSequence summary = "hello"; + StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, + true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, + false /* lights */, false /* defaultLights */, groupId /* group */); + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); + + assertThat(record.getSummarization()).isNull(); + + Bundle signals = new Bundle(); + signals.putCharSequence(Adjustment.KEY_SUMMARIZATION, summary); + record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId())); + + record.applyAdjustments(); + + assertThat(record.getSummarization()).isEqualTo(summary.toString()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_SUMMARIZATION) + public void testSummarization_string() { + String summary = "hello"; + StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, + true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, + false /* lights */, false /* defaultLights */, groupId /* group */); + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); + + assertThat(record.getSummarization()).isNull(); + + Bundle signals = new Bundle(); + signals.putCharSequence(Adjustment.KEY_SUMMARIZATION, summary); + record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId())); + + record.applyAdjustments(); + + assertThat(record.getSummarization()).isEqualTo(summary); + } + + @Test public void testSensitiveContent() { StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, 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 752910d6d3c1..a02f628ce9b7 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -46,13 +46,11 @@ import static android.app.NotificationManager.IMPORTANCE_MAX; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import static android.app.NotificationManager.VISIBILITY_NO_OVERRIDE; -import static android.content.ContentResolver.SCHEME_ANDROID_RESOURCE; -import static android.content.ContentResolver.SCHEME_CONTENT; -import static android.content.ContentResolver.SCHEME_FILE; import static android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; + import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.service.notification.Adjustment.TYPE_CONTENT_RECOMMENDATION; import static android.service.notification.Adjustment.TYPE_NEWS; @@ -66,6 +64,7 @@ import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.No import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; +import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA; import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER; import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE; @@ -92,7 +91,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -385,10 +383,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, false, mClock); + false, mClock); mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, false, mClock); + false, mClock); resetZenModeHelper(); mAudioAttributes = new AudioAttributes.Builder() @@ -803,7 +801,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_oldXml_migrates() throws Exception { mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); + /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"2\">\n" + "<package name=\"" + PKG_N_MR1 + "\" uid=\"" + UID_N_MR1 @@ -939,7 +937,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); + /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"3\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -998,7 +996,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_permissionNotificationOff() throws Exception { mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock); + /* showReviewPermissionsNotification= */ false, mClock); String xml = "<ranking version=\"3\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -1057,7 +1055,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); + /* showReviewPermissionsNotification= */ true, mClock); String xml = "<ranking version=\"4\">\n" + "<package name=\"" + PKG_N_MR1 + "\" show_badge=\"true\">\n" @@ -1655,7 +1653,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // simulate load after reboot mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, false, mClock); + false, mClock); loadByteArrayXml(baos.toByteArray(), false, USER_ALL); // Trigger 2nd restore pass @@ -1710,7 +1708,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // simulate load after reboot mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, false, mClock); + false, mClock); loadByteArrayXml(xml.getBytes(), false, USER_ALL); // Trigger 2nd restore pass @@ -1788,10 +1786,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, false, mClock); + false, mClock); mXmlHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, - mUgmInternal, false, mClock); + false, mClock); NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_LOW); @@ -3263,61 +3261,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() { - final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri"); - - doThrow(new SecurityException("no access")).when(mUgmInternal) - .checkGrantUriPermission(eq(UID_N_MR1), any(), eq(sound), - anyInt(), eq(UserHandle.getUserId(UID_N_MR1))); - - final NotificationChannel channel = new NotificationChannel("id2", "name2", - NotificationManager.IMPORTANCE_DEFAULT); - channel.setSound(sound, mAudioAttributes); - - assertThrows(SecurityException.class, - () -> mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, - true, false, UID_N_MR1, false)); - assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true)) - .isNull(); - } - - @Test - public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() { - final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound"); - - doThrow(new SecurityException("no access")).when(mUgmInternal) - .checkGrantUriPermission(eq(UID_N_MR1), any(), any(), - anyInt(), eq(UserHandle.getUserId(UID_N_MR1))); - - final NotificationChannel channel = new NotificationChannel("id2", "name2", - NotificationManager.IMPORTANCE_DEFAULT); - channel.setSound(sound, mAudioAttributes); - - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, - false); - assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true) - .getSound()).isEqualTo(sound); - } - - @Test - public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() { - final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound"); - - doThrow(new SecurityException("no access")).when(mUgmInternal) - .checkGrantUriPermission(eq(UID_N_MR1), any(), any(), - anyInt(), eq(UserHandle.getUserId(UID_N_MR1))); - - final NotificationChannel channel = new NotificationChannel("id2", "name2", - NotificationManager.IMPORTANCE_DEFAULT); - channel.setSound(sound, mAudioAttributes); - - mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, - false); - assertThat(mHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel.getId(), true) - .getSound()).isEqualTo(sound); - } - - @Test public void testPermanentlyDeleteChannels() throws Exception { NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); @@ -7086,10 +7029,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager, NotificationChannelLogger notificationChannelLogger, AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles, - UriGrantsManagerInternal ugmInternal, boolean showReviewPermissionsNotification, Clock clock) { super(context, pm, rankingHandler, zenHelper, permHelper, permManager, - notificationChannelLogger, appOpsManager, userProfiles, ugmInternal, + notificationChannelLogger, appOpsManager, userProfiles, showReviewPermissionsNotification, clock); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 51891ef71bbd..5377102e5a13 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -203,6 +203,7 @@ import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.InvalidProtocolBufferException; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -2478,6 +2479,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @Ignore("TODO: b/398023814 - disabled due to taking a long time; restore when we have a " + + "better approach to not timing out") public void testAddAutomaticZenRule_claimedSystemOwner() { // Make sure anything that claims to have a "system" owner but not actually part of the // system package still gets limited on number of rules diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 6d8a48799112..3ca019728c2b 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -150,8 +150,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT}, KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DPAD_LEFT, META_ON}, - {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL}, - KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DEL, META_ON}, {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH}, KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyEvent.KEYCODE_APP_SWITCH, 0}, diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index d016e735f0c7..8fe08553db95 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -251,10 +251,6 @@ class AppCompatActivityRobot { doReturn(mTaskStack.top()).when(mActivityStack.top()).getOrganizedTask(); } - void setIsInLetterboxAnimation(boolean inAnimation) { - doReturn(inAnimation).when(mActivityStack.top()).isInLetterboxAnimation(); - } - void setTopTaskInMultiWindowMode(boolean inMultiWindowMode) { doReturn(inMultiWindowMode).when(mTaskStack.top()).inMultiWindowMode(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java index d38f3b09c4fa..7bcf4eaa7c8b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java @@ -66,7 +66,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { runTestScenario((robot) -> { robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); @@ -87,7 +86,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { runTestScenario((robot) -> { robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ false); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); @@ -109,7 +107,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { runTestScenario((robot) -> { robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); @@ -131,7 +128,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { runTestScenario((robot) -> { robot.configureWindowStateWithTaskBar(/* hasTaskBarInsetsRoundedCorners */ true); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); @@ -157,7 +153,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { robot.conf().setLetterboxActivityCornersRadius(-1); robot.configureWindowState(); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); @@ -184,25 +179,17 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { robot.conf().setLetterboxActivityCornersRadius(15); robot.configureWindowState(); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); robot.resources().configureGetDimensionPixelSize(R.dimen.taskbar_frame_height, 20); robot.setInvCompatState(/* scale */ 0.5f); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ true); - robot.checkWindowStateRoundedCornersRadius(/* expected */ 7); - - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.checkWindowStateRoundedCornersRadius(/* expected */ 7); robot.activity().setTopActivityVisibleRequested(/* isVisibleRequested */ false); robot.activity().setTopActivityVisible(/* isVisible */ false); robot.checkWindowStateRoundedCornersRadius(/* expected */ 0); - - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ true); - robot.checkWindowStateRoundedCornersRadius(/* expected */ 7); }); } @@ -212,7 +199,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { robot.conf().setLetterboxActivityCornersRadius(15); robot.configureWindowState(); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); @@ -236,7 +222,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { robot.conf().setLetterboxActivityCornersRadius(15); robot.configureWindowState(); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); @@ -260,7 +245,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { robot.conf().setLetterboxActivityCornersRadius(15); robot.configureWindowState(); robot.activity().createActivityWithComponent(); - robot.setTopActivityInLetterboxAnimation(/* inLetterboxAnimation */ false); robot.activity().setTopActivityVisible(/* isVisible */ true); robot.setIsLetterboxedForFixedOrientationAndAspectRatio(/* inLetterbox */ true); robot.conf().setLetterboxActivityCornersRounded(/* rounded */ true); @@ -362,10 +346,6 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { mWindowState.mInvGlobalScale = scale; } - void setTopActivityInLetterboxAnimation(boolean inLetterboxAnimation) { - doReturn(inLetterboxAnimation).when(activity().top()).isInLetterboxAnimation(); - } - void setTopActivityTransparentPolicyRunning(boolean running) { doReturn(running).when(getTransparentPolicy()).isRunning(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxUtilsTest.java index a4a63d266725..ac707d23b492 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxUtilsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxUtilsTest.java @@ -63,25 +63,10 @@ public class AppCompatLetterboxUtilsTest extends WindowTestsBase { } @Test - public void positionIsFromTaskWhenLetterboxAnimationIsRunning() { + public void positionIsFromActivity() { runTestScenario((robot) -> { robot.activity().createActivityWithComponent(); robot.setTopActivityLetterboxPolicyRunning(true); - robot.activity().setIsInLetterboxAnimation(true); - robot.activity().configureTaskBounds( - new Rect(/* left */ 100, /* top */ 200, /* right */ 300, /* bottom */ 400)); - robot.getLetterboxPosition(); - - robot.assertPosition(/* x */ 100, /* y */ 200); - }); - } - - @Test - public void positionIsFromActivityWhenLetterboxAnimationIsNotRunning() { - runTestScenario((robot) -> { - robot.activity().createActivityWithComponent(); - robot.setTopActivityLetterboxPolicyRunning(true); - robot.activity().setIsInLetterboxAnimation(false); robot.activity().configureTopActivityBounds( new Rect(/* left */ 200, /* top */ 400, /* right */ 300, /* bottom */ 400)); robot.getLetterboxPosition(); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index e08197155f03..bdee3c323549 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -225,10 +225,10 @@ public class BackNavigationControllerTests extends WindowTestsBase { CrossActivityTestCase testCase = createTopTaskWithTwoActivities(); IOnBackInvokedCallback callback = withSystemCallback(testCase.task); testCase.windowFront.mAttrs.windowAnimations = 0x10; - spyOn(mDisplayContent.mAppTransition.mTransitionAnimation); - doReturn(0xffff00AB).when(mDisplayContent.mAppTransition.mTransitionAnimation) + spyOn(mDisplayContent.mTransitionAnimation); + doReturn(0xffff00AB).when(mDisplayContent.mTransitionAnimation) .getAnimationResId(any(), anyInt(), anyInt()); - doReturn(0xffff00CD).when(mDisplayContent.mAppTransition.mTransitionAnimation) + doReturn(0xffff00CD).when(mDisplayContent.mTransitionAnimation) .getDefaultAnimationResId(anyInt(), anyInt()); BackNavigationInfo backNavigationInfo = startBackNavigation(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 012a22358891..4c81f738138a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -87,7 +87,7 @@ import static com.android.server.wm.TransitionSubject.assertThat; import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE; import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT; -import static com.android.window.flags.Flags.FLAG_ENABLE_PERSISTING_DENSITY_SCALE_FOR_CONNECTED_DISPLAYS; +import static com.android.window.flags.Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS; import static com.google.common.truth.Truth.assertThat; @@ -2925,7 +2925,7 @@ public class DisplayContentTests extends WindowTestsBase { assertFalse(dc.mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(dc)); } - @EnableFlags(FLAG_ENABLE_PERSISTING_DENSITY_SCALE_FOR_CONNECTED_DISPLAYS) + @EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test public void testForcedDensityRatioSetForExternalDisplays_persistDensityScaleFlagEnabled() { final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); @@ -2955,7 +2955,7 @@ public class DisplayContentTests extends WindowTestsBase { displayContent.mExternalDisplayForcedDensityRatio, 0.01); } - @EnableFlags(FLAG_ENABLE_PERSISTING_DENSITY_SCALE_FOR_CONNECTED_DISPLAYS) + @EnableFlags(FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test public void testForcedDensityUpdateForExternalDisplays_persistDensityScaleFlagEnabled() { final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index 4854f0d948b4..862158e8a0a1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -362,7 +362,19 @@ public class DisplayPolicyTests extends WindowTestsBase { @Test public void testSwitchDecorInsets() { - createNavBarWithProvidedInsets(mDisplayContent); + final WindowState win = createApplicationWindow(); + final WindowState bar = createNavBarWithProvidedInsets(mDisplayContent); + bar.getFrame().set(0, mDisplayContent.mDisplayFrames.mHeight - NAV_BAR_HEIGHT, + mDisplayContent.mDisplayFrames.mWidth, mDisplayContent.mDisplayFrames.mHeight); + final int insetsId = bar.mAttrs.providedInsets[0].getId(); + final InsetsSourceProvider provider = mDisplayContent.getInsetsStateController() + .getOrCreateSourceProvider(insetsId, bar.mAttrs.providedInsets[0].getType()); + provider.setServerVisible(true); + provider.updateSourceFrame(bar.getFrame()); + + final InsetsState prevInsetsState = new InsetsState(); + prevInsetsState.addSource(new InsetsSource(provider.getSource())); + final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy(); final DisplayInfo info = mDisplayContent.getDisplayInfo(); final int w = info.logicalWidth; @@ -385,6 +397,18 @@ public class DisplayPolicyTests extends WindowTestsBase { // The current insets are restored from cache directly. assertEquals(prevConfigFrame, displayPolicy.getDecorInsetsInfo(info.rotation, info.logicalWidth, info.logicalHeight).mConfigFrame); + // Assume that the InsetsSource in current InsetsState is not updated yet. And it will be + // replaced by the one in cache. + InsetsState currentInsetsState = new InsetsState(); + final InsetsSource prevSource = new InsetsSource(provider.getSource()); + prevSource.getFrame().scale(0.5f); + currentInsetsState.addSource(prevSource); + currentInsetsState = mDisplayContent.getInsetsPolicy().adjustInsetsForWindow( + win, currentInsetsState); + if (com.android.window.flags.Flags.useCachedInsetsForDisplaySwitch()) { + assertEquals(prevInsetsState.peekSource(insetsId), + currentInsetsState.peekSource(insetsId)); + } // If screen is not fully turned on, then the cache should be preserved. displayPolicy.screenTurnedOff(false /* acquireSleepToken */); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java index a57577a96f79..81e487a67725 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java @@ -274,7 +274,7 @@ public class DisplayWindowSettingsTests extends WindowTestsBase { mSecondaryDisplay.mBaseDisplayDensity); } - @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DENSITY_SCALE_FOR_CONNECTED_DISPLAYS) + @EnableFlags(Flags.FLAG_ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS) @Test public void testSetForcedDensityRatio() { mDisplayWindowSettings.setForcedDensity(mSecondaryDisplay.getDisplayInfo(), diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxAttachInputTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxAttachInputTest.java index 51e02405e489..8ee5999e7c24 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxAttachInputTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxAttachInputTest.java @@ -38,7 +38,6 @@ import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.InputWindowHandle; -import android.view.SurfaceControl; import android.view.WindowManager; import com.android.server.testutils.StubTransaction; @@ -76,8 +75,7 @@ public class LetterboxAttachInputTest extends WindowTestsBase { doReturn(0.5f).when(letterboxOverrides).getLetterboxWallpaperDarkScrimAlpha(); mWindowState = createWindowState(); mLetterbox = new Letterbox(mSurfaces, StubTransaction::new, - mock(AppCompatReachabilityPolicy.class), letterboxOverrides, - () -> mock(SurfaceControl.class)); + mock(AppCompatReachabilityPolicy.class), letterboxOverrides); mTransaction = spy(StubTransaction.class); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java index a51a44f6167b..e5533b659e03 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java @@ -68,7 +68,6 @@ public class LetterboxTest { private SurfaceControlMocker mSurfaces; private SurfaceControl.Transaction mTransaction; - private SurfaceControl mParentSurface = mock(SurfaceControl.class); private AppCompatLetterboxOverrides mLetterboxOverrides; private WindowState mWindowState; @@ -84,7 +83,7 @@ public class LetterboxTest { doReturn(0.5f).when(mLetterboxOverrides).getLetterboxWallpaperDarkScrimAlpha(); mWindowState = mock(WindowState.class); mLetterbox = new Letterbox(mSurfaces, StubTransaction::new, - mock(AppCompatReachabilityPolicy.class), mLetterboxOverrides, () -> mParentSurface); + mock(AppCompatReachabilityPolicy.class), mLetterboxOverrides); mTransaction = spy(StubTransaction.class); } @@ -259,22 +258,6 @@ public class LetterboxTest { } @Test - public void testNeedsApplySurfaceChanges_setParentSurface() { - mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000)); - applySurfaceChanges(); - - verify(mTransaction).reparent(mSurfaces.top, mParentSurface); - assertFalse(mLetterbox.needsApplySurfaceChanges()); - - mParentSurface = mock(SurfaceControl.class); - - assertTrue(mLetterbox.needsApplySurfaceChanges()); - - applySurfaceChanges(); - verify(mTransaction).reparent(mSurfaces.top, mParentSurface); - } - - @Test public void testApplySurfaceChanges_cornersNotRounded_surfaceFullWindowSurfaceNotCreated() { mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000)); applySurfaceChanges(); diff --git a/services/usb/java/com/android/server/usb/UsbManagerInternal.java b/services/usb/java/com/android/server/usb/UsbManagerInternal.java deleted file mode 100644 index 31c5986c45b8..000000000000 --- a/services/usb/java/com/android/server/usb/UsbManagerInternal.java +++ /dev/null @@ -1,50 +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.usb; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.hardware.usb.IUsbOperationInternal; -import android.hardware.usb.UsbPort; -import android.util.ArraySet; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * UsbManagerInternal provides internal APIs for the UsbService to - * reduce IPC overhead costs and support internal USB data signal stakers. - * - * @hide Only for use within the system server. - */ -public abstract class UsbManagerInternal { - - public static final int OS_USB_DISABLE_REASON_AAPM = 0; - public static final int OS_USB_DISABLE_REASON_LOCKDOWN_MODE = 1; - - @Retention(RetentionPolicy.SOURCE) - @IntDef(value = {OS_USB_DISABLE_REASON_AAPM, - OS_USB_DISABLE_REASON_LOCKDOWN_MODE}) - public @interface OsUsbDisableReason { - } - - public abstract boolean enableUsbData(String portId, boolean enable, - int operationId, IUsbOperationInternal callback, @OsUsbDisableReason int disableReason); - - public abstract UsbPort[] getPorts(); - -}
\ No newline at end of file diff --git a/services/usb/java/com/android/server/usb/UsbService.java b/services/usb/java/com/android/server/usb/UsbService.java index 4395b76d91cc..7808b2e318b2 100644 --- a/services/usb/java/com/android/server/usb/UsbService.java +++ b/services/usb/java/com/android/server/usb/UsbService.java @@ -27,7 +27,10 @@ import static android.hardware.usb.UsbPortStatus.MODE_UFP; import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SINK; import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SOURCE; +import android.hardware.usb.IUsbManagerInternal; + import android.annotation.NonNull; +import android.annotation.IntDef; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; @@ -80,12 +83,16 @@ import dalvik.annotation.optimization.NeverCompile; import java.io.File; import java.io.FileDescriptor; 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.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + /** * UsbService manages all USB related state, including both host and device support. * Host related events and calls are delegated to UsbHostManager, and device related @@ -93,6 +100,15 @@ import java.util.concurrent.CompletableFuture; */ public class UsbService extends IUsbManager.Stub { + public static final int OS_USB_DISABLE_REASON_AAPM = 0; + public static final int OS_USB_DISABLE_REASON_LOCKDOWN_MODE = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {OS_USB_DISABLE_REASON_AAPM, + OS_USB_DISABLE_REASON_LOCKDOWN_MODE}) + public @interface OsUsbDisableReason { + } + public static class Lifecycle extends SystemService { private UsbService mUsbService; private final CompletableFuture<Void> mOnStartFinished = new CompletableFuture<>(); @@ -227,7 +243,7 @@ public class UsbService extends IUsbManager.Stub { filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); mContext.registerReceiverAsUser(receiver, UserHandle.ALL, filter, null, null); if(android.hardware.usb.flags.Flags.enableUsbDataSignalStakingInternal()) { - LocalServices.addService(UsbManagerInternal.class, new UsbManagerInternalImpl()); + LocalServices.addService(IUsbManagerInternal.class, new UsbManagerInternalImpl()); } } @@ -246,7 +262,7 @@ public class UsbService extends IUsbManager.Stub { mPermissionManager = new UsbPermissionManager(context, this); if(android.hardware.usb.flags.Flags.enableUsbDataSignalStakingInternal()) { - LocalServices.addService(UsbManagerInternal.class, new UsbManagerInternalImpl()); + LocalServices.addService(IUsbManagerInternal.class, new UsbManagerInternalImpl()); } } @@ -1536,24 +1552,30 @@ public class UsbService extends IUsbManager.Stub { enableUsbDataInternal(port.getId(), !lockDownTriggeredByUser, STRONG_AUTH_OPERATION_ID, new IUsbOperationInternal.Default(), - UsbManagerInternal.OS_USB_DISABLE_REASON_LOCKDOWN_MODE, + OS_USB_DISABLE_REASON_LOCKDOWN_MODE, true); } } } - private class UsbManagerInternalImpl extends UsbManagerInternal { - @Override - public boolean enableUsbData(String portId, boolean enable, - int operationId, IUsbOperationInternal callback, - @OsUsbDisableReason int disableReason) { - return enableUsbDataInternal(portId, enable, operationId, callback, - disableReason, true); - } + private class UsbManagerInternalImpl extends IUsbManagerInternal.Stub { + private static final AtomicInteger sUsbOperationCount = new AtomicInteger(); @Override - public UsbPort[] getPorts() { - return mPortManager.getPorts(); + public boolean enableUsbDataSignal(boolean enable, + @OsUsbDisableReason int disableReason) { + boolean result = true; + int operationId = sUsbOperationCount.incrementAndGet() + disableReason; + for (UsbPort port : mPortManager.getPorts()) { + boolean success = enableUsbDataInternal(port.getId(), enable, operationId, + new IUsbOperationInternal.Default(), disableReason, true); + if(!success) { + Slog.e(TAG, "enableUsbDataInternal failed to change USB port " + + port.getId() + "state to " + enable); + } + result &= success; + } + return result; } } } diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index 531f51604507..9e57fd3c1a7b 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -30,6 +30,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.ParcelFileDescriptor; +import android.os.UserHandle; import com.android.internal.telecom.IVideoProvider; import com.android.server.telecom.flags.Flags; @@ -680,6 +681,7 @@ public final class Call { private final @CallDirection int mCallDirection; private final @Connection.VerificationStatus int mCallerNumberVerificationStatus; private final Uri mContactPhotoUri; + private final UserHandle mAssociatedUser; /** * Whether the supplied capabilities supports the specified capability. @@ -1081,6 +1083,16 @@ public final class Call { return mCallerNumberVerificationStatus; } + /** + * Gets the user that originated the call + * @return The user + * + * @hide + */ + public UserHandle getAssociatedUser() { + return mAssociatedUser; + } + @Override public boolean equals(Object o) { if (o instanceof Details) { @@ -1107,7 +1119,8 @@ public final class Call { Objects.equals(mCallDirection, d.mCallDirection) && Objects.equals(mCallerNumberVerificationStatus, d.mCallerNumberVerificationStatus) && - Objects.equals(mContactPhotoUri, d.mContactPhotoUri); + Objects.equals(mContactPhotoUri, d.mContactPhotoUri) && + Objects.equals(mAssociatedUser, d.mAssociatedUser); } return false; } @@ -1133,7 +1146,8 @@ public final class Call { mContactDisplayName, mCallDirection, mCallerNumberVerificationStatus, - mContactPhotoUri); + mContactPhotoUri, + mAssociatedUser); } /** {@hide} */ @@ -1158,7 +1172,8 @@ public final class Call { String contactDisplayName, int callDirection, int callerNumberVerificationStatus, - Uri contactPhotoUri) { + Uri contactPhotoUri, + UserHandle originatingUser) { mState = state; mTelecomCallId = telecomCallId; mHandle = handle; @@ -1180,6 +1195,7 @@ public final class Call { mCallDirection = callDirection; mCallerNumberVerificationStatus = callerNumberVerificationStatus; mContactPhotoUri = contactPhotoUri; + mAssociatedUser = originatingUser; } /** {@hide} */ @@ -1205,7 +1221,8 @@ public final class Call { parcelableCall.getContactDisplayName(), parcelableCall.getCallDirection(), parcelableCall.getCallerNumberVerificationStatus(), - parcelableCall.getContactPhotoUri() + parcelableCall.getContactPhotoUri(), + parcelableCall.getAssociatedUser() ); } @@ -2631,7 +2648,8 @@ public final class Call { mDetails.getContactDisplayName(), mDetails.getCallDirection(), mDetails.getCallerNumberVerificationStatus(), - mDetails.getContactPhotoUri() + mDetails.getContactPhotoUri(), + mDetails.getAssociatedUser() ); fireDetailsChanged(mDetails); } diff --git a/telecomm/java/android/telecom/ParcelableCall.java b/telecomm/java/android/telecom/ParcelableCall.java index 6a1318982e77..bd004e5e6231 100644 --- a/telecomm/java/android/telecom/ParcelableCall.java +++ b/telecomm/java/android/telecom/ParcelableCall.java @@ -16,14 +16,17 @@ package android.telecom; +import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; +import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; +import android.os.UserHandle; import android.telecom.Call.Details.CallDirection; import com.android.internal.telecom.IVideoProvider; @@ -70,6 +73,7 @@ public final class ParcelableCall implements Parcelable { private String mContactDisplayName; private String mActiveChildCallId; private Uri mContactPhotoUri; + private UserHandle mAssociatedUser; public ParcelableCallBuilder setId(String id) { mId = id; @@ -230,6 +234,11 @@ public final class ParcelableCall implements Parcelable { return this; } + public ParcelableCallBuilder setAssociatedUser(UserHandle user) { + mAssociatedUser = user; + return this; + } + public ParcelableCall createParcelableCall() { return new ParcelableCall( mId, @@ -262,7 +271,8 @@ public final class ParcelableCall implements Parcelable { mCallerNumberVerificationStatus, mContactDisplayName, mActiveChildCallId, - mContactPhotoUri); + mContactPhotoUri, + mAssociatedUser); } public static ParcelableCallBuilder fromParcelableCall(ParcelableCall parcelableCall) { @@ -300,6 +310,7 @@ public final class ParcelableCall implements Parcelable { newBuilder.mContactDisplayName = parcelableCall.mContactDisplayName; newBuilder.mActiveChildCallId = parcelableCall.mActiveChildCallId; newBuilder.mContactPhotoUri = parcelableCall.mContactPhotoUri; + newBuilder.mAssociatedUser = parcelableCall.mAssociatedUser; return newBuilder; } } @@ -336,6 +347,7 @@ public final class ParcelableCall implements Parcelable { private final String mContactDisplayName; private final String mActiveChildCallId; // Only valid for CDMA conferences private final Uri mContactPhotoUri; + private final UserHandle mAssociatedUser; public ParcelableCall( String id, @@ -368,7 +380,8 @@ public final class ParcelableCall implements Parcelable { int callerNumberVerificationStatus, String contactDisplayName, String activeChildCallId, - Uri contactPhotoUri + Uri contactPhotoUri, + UserHandle associatedUser ) { mId = id; mState = state; @@ -401,6 +414,7 @@ public final class ParcelableCall implements Parcelable { mContactDisplayName = contactDisplayName; mActiveChildCallId = activeChildCallId; mContactPhotoUri = contactPhotoUri; + mAssociatedUser = associatedUser; } /** The unique ID of the call. */ @@ -624,6 +638,13 @@ public final class ParcelableCall implements Parcelable { return mContactPhotoUri; } + /** + * @return the originating user + */ + public @NonNull UserHandle getAssociatedUser() { + return mAssociatedUser; + } + /** * @return On a CDMA conference with two participants, returns the ID of the child call that's @@ -666,6 +687,9 @@ public final class ParcelableCall implements Parcelable { source.readList(conferenceableCallIds, classLoader, java.lang.String.class); Bundle intentExtras = source.readBundle(classLoader); Bundle extras = source.readBundle(classLoader); + if (extras == null) { + extras = new Bundle(); + } int supportedAudioRoutes = source.readInt(); boolean isRttCallChanged = source.readByte() == 1; ParcelableRttCall rttCall = source.readParcelable(classLoader, android.telecom.ParcelableRttCall.class); @@ -675,6 +699,8 @@ public final class ParcelableCall implements Parcelable { String contactDisplayName = source.readString(); String activeChildCallId = source.readString(); Uri contactPhotoUri = source.readParcelable(classLoader, Uri.class); + UserHandle associatedUser = source.readParcelable(classLoader, UserHandle.class); + extras.putParcelable(Intent.EXTRA_USER_HANDLE, associatedUser); return new ParcelableCallBuilder() .setId(id) .setState(state) @@ -707,6 +733,7 @@ public final class ParcelableCall implements Parcelable { .setContactDisplayName(contactDisplayName) .setActiveChildCallId(activeChildCallId) .setContactPhotoUri(contactPhotoUri) + .setAssociatedUser(associatedUser) .createParcelableCall(); } @@ -757,6 +784,7 @@ public final class ParcelableCall implements Parcelable { destination.writeString(mContactDisplayName); destination.writeString(mActiveChildCallId); destination.writeParcelable(mContactPhotoUri, 0); + destination.writeParcelable(mAssociatedUser, 0); } @Override diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 58833e8f8141..50c5a6b68c7b 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -10962,7 +10962,7 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_MMS_SMS_TO_MMS_TEXT_THRESHOLD_INT, -1); sDefaults.putInt(KEY_MMS_SUBJECT_MAX_LENGTH_INT, 40); sDefaults.putInt(KEY_MMS_NETWORK_RELEASE_TIMEOUT_MILLIS_INT, 5 * 1000); - sDefaults.putInt(KEY_MMS_MAX_NTN_PAYLOAD_SIZE_BYTES_INT, 3 * 1000); + sDefaults.putInt(KEY_MMS_MAX_NTN_PAYLOAD_SIZE_BYTES_INT, -1); sDefaults.putString(KEY_MMS_EMAIL_GATEWAY_NUMBER_STRING, ""); sDefaults.putString(KEY_MMS_HTTP_PARAMS_STRING, ""); sDefaults.putString(KEY_MMS_NAI_SUFFIX_STRING, ""); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index d2741ac7ee9f..e04d285d6632 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1187,6 +1187,61 @@ public class SubscriptionManager { public static final String IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM = SimInfo.COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM; + /** + * TelephonyProvider column name for satellite entitlement barred plmns. The value of this + * column is set based on entitlement query result for satellite configuration. + * By default, it's empty. + * <P>Type: TEXT </P> + * + * @hide + */ + public static final String SATELLITE_ENTITLEMENT_BARRED_PLMNS = + SimInfo.COLUMN_SATELLITE_ENTITLEMENT_BARRED_PLMNS; + + /** + * TelephonyProvider column name for satellite entitlement data plan for plmns. The value + * of this column is set based on entitlement query result for satellite configuration. + * By default, it's empty. + * <P>Type: TEXT </P> + * + * @hide + */ + public static final String SATELLITE_ENTITLEMENT_DATA_PLAN_PLMNS = + SimInfo.COLUMN_SATELLITE_ENTITLEMENT_DATA_PLAN_PLMNS; + + /** + * TelephonyProvider column name for satellite entitlement service type map. The value of + * this column is set based on entitlement query result for satellite configuration. + * By default, it's empty. + * <P>Type: TEXT </P> + * + * @hide + */ + public static final String SATELLITE_ENTITLEMENT_SERVICE_TYPE_MAP = + SimInfo.COLUMN_SATELLITE_ENTITLEMENT_SERVICE_TYPE_MAP; + + /** + * TelephonyProvider column name for satellite entitlement data service policy. The value + * of this column is set based on entitlement query result for satellite configuration. + * By default, it's empty. + * <P>Type: TEXT </P> + * + * @hide + */ + public static final String SATELLITE_ENTITLEMENT_DATA_SERVICE_POLICY = + SimInfo.COLUMN_SATELLITE_ENTITLEMENT_DATA_SERVICE_POLICY; + + /** + * TelephonyProvider column name for satellite entitlement voice service policy. The value + * of this column is set based on entitlement query result for satellite configuration. + * By default, it's empty. + * <P>Type: TEXT </P> + * + * @hide + */ + public static final String SATELLITE_ENTITLEMENT_VOICE_SERVICE_POLICY = + SimInfo.COLUMN_SATELLITE_ENTITLEMENT_VOICE_SERVICE_POLICY; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"USAGE_SETTING_"}, diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java index 3498974b348e..229c7bfb53e9 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java +++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java @@ -103,6 +103,10 @@ public class IntegrationTests { @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void reportJankStats_confirmPendingStatsIncreases() { Activity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null); + mDevice.wait(Until.findObject( + By.text(jankTrackerActivity.getString(R.string.continue_test))), + WAIT_FOR_TIMEOUT_MS); + EditText editText = jankTrackerActivity.findViewById(R.id.edit_text); JankTracker jankTracker = editText.getJankTracker(); @@ -135,6 +139,10 @@ public class IntegrationTests { public void simulateWidgetStateChanges_confirmStateChangesAreTracked() { JankTrackerActivity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null); + mDevice.wait(Until.findObject( + By.text(jankTrackerActivity.getString(R.string.continue_test))), + WAIT_FOR_TIMEOUT_MS); + TestWidget testWidget = jankTrackerActivity.findViewById(R.id.jank_tracker_widget); JankTracker jankTracker = testWidget.getJankTracker(); jankTracker.forceListenerRegistration(); diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt index d7f91e009c92..2093ccc86653 100644 --- a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt +++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt @@ -156,19 +156,6 @@ class SeamlessAppRotationTest(flicker: LegacyFlickerTest) : RotationTransition(f flicker.assertLayers { isVisible(testApp) } } - /** Checks that [testApp] layer covers the entire screen during the whole transition */ - @Presubmit - @Test - fun appLayerRotates() { - flicker.assertLayers { - this.invoke("entireScreenCovered") { entry -> - entry.entry.displays.map { display -> - entry.visibleRegion(testApp).coversExactly(display.layerStackSpace) - } - } - } - } - /** {@inheritDoc} */ @Test @Ignore("Not applicable to this CUJ. App is full screen") @@ -225,7 +212,6 @@ class SeamlessAppRotationTest(flicker: LegacyFlickerTest) : RotationTransition(f visibleLayersShownMoreThanOneConsecutiveEntry() visibleWindowsShownMoreThanOneConsecutiveEntry() - runAndIgnoreAssumptionViolation { appLayerRotates() } runAndIgnoreAssumptionViolation { appLayerAlwaysVisible() } runAndIgnoreAssumptionViolation { navBarLayerIsVisibleAtStartAndEnd() } runAndIgnoreAssumptionViolation { navBarWindowIsAlwaysVisible() } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 55d6fd9b4a73..6d8ea409f6f2 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -28,10 +28,13 @@ import android.tools.device.apphelpers.IStandardAppHelper import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.parsers.WindowManagerStateHelper import android.tools.traces.wm.WindowingMode +import android.view.KeyEvent.KEYCODE_DPAD_DOWN +import android.view.KeyEvent.KEYCODE_DPAD_UP import android.view.KeyEvent.KEYCODE_EQUALS import android.view.KeyEvent.KEYCODE_LEFT_BRACKET import android.view.KeyEvent.KEYCODE_MINUS import android.view.KeyEvent.KEYCODE_RIGHT_BRACKET +import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_META_ON import android.view.WindowInsets import android.view.WindowManager @@ -151,19 +154,42 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ?: error("Unable to find resource $MAXIMIZE_BUTTON_VIEW\n") } - /** Click maximise button on the app header for the given app. */ + /** Maximize a given app to fill the stable bounds. */ fun maximiseDesktopApp( wmHelper: WindowManagerStateHelper, device: UiDevice, - usingKeyboard: Boolean = false + trigger: MaximizeDesktopAppTrigger = MaximizeDesktopAppTrigger.MAXIMIZE_MENU, ) { - if (usingKeyboard) { - val keyEventHelper = KeyEventHelper(getInstrumentation()) - keyEventHelper.press(KEYCODE_EQUALS, META_META_ON) - } else { - val caption = getCaptionForTheApp(wmHelper, device) - val maximizeButton = getMaximizeButtonForTheApp(caption) - maximizeButton.click() + val caption = getCaptionForTheApp(wmHelper, device)!! + val maximizeButton = getMaximizeButtonForTheApp(caption) + + when (trigger) { + MaximizeDesktopAppTrigger.MAXIMIZE_MENU -> maximizeButton.click() + MaximizeDesktopAppTrigger.DOUBLE_TAP_APP_HEADER -> { + caption.click() + Thread.sleep(50) + caption.click() + } + + MaximizeDesktopAppTrigger.KEYBOARD_SHORTCUT -> { + val keyEventHelper = KeyEventHelper(getInstrumentation()) + keyEventHelper.press(KEYCODE_EQUALS, META_META_ON) + } + + MaximizeDesktopAppTrigger.MAXIMIZE_BUTTON_IN_MENU -> { + maximizeButton.longClick() + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + val buttonResId = MAXIMIZE_BUTTON_IN_MENU + val maximizeMenu = getDesktopAppViewByRes(MAXIMIZE_MENU) + val maximizeButtonInMenu = + maximizeMenu + ?.wait( + Until.findObject(By.res(SYSTEMUI_PACKAGE, buttonResId)), + TIMEOUT.toMillis() + ) + ?: error("Unable to find object with resource id $buttonResId") + maximizeButtonInMenu.click() + } } wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() } @@ -472,6 +498,22 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : device.drag(startX, startY, endX, endY, 100) } + fun enterDesktopModeViaKeyboard( + wmHelper: WindowManagerStateHelper, + ) { + val keyEventHelper = KeyEventHelper(getInstrumentation()) + keyEventHelper.press(KEYCODE_DPAD_DOWN, META_META_ON or META_CTRL_ON) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + } + + fun exitDesktopModeToFullScreenViaKeyboard( + wmHelper: WindowManagerStateHelper, + ) { + val keyEventHelper = KeyEventHelper(getInstrumentation()) + keyEventHelper.press(KEYCODE_DPAD_UP, META_META_ON or META_CTRL_ON) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + } + fun enterDesktopModeFromAppHandleMenu( wmHelper: WindowManagerStateHelper, device: UiDevice @@ -550,6 +592,13 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : rightSideMatching } + enum class MaximizeDesktopAppTrigger { + MAXIMIZE_MENU, + DOUBLE_TAP_APP_HEADER, + KEYBOARD_SHORTCUT, + MAXIMIZE_BUTTON_IN_MENU + } + private companion object { val TIMEOUT: Duration = Duration.ofSeconds(3) const val SNAP_RESIZE_DRAG_INSET: Int = 5 // inset to avoid dragging to display edge @@ -561,6 +610,7 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : const val DESKTOP_MODE_BUTTON: String = "desktop_button" const val SNAP_LEFT_BUTTON: String = "maximize_menu_snap_left_button" const val SNAP_RIGHT_BUTTON: String = "maximize_menu_snap_right_button" + const val MAXIMIZE_BUTTON_IN_MENU: String = "maximize_menu_size_toggle_button" const val MINIMIZE_BUTTON_VIEW: String = "minimize_window" const val HEADER_EMPTY_VIEW: String = "caption_handle" val caption: BySelector diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index 7c24a4adca3d..98af8b2f53b5 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -39,7 +39,6 @@ android:value="true" /> <activity android:name=".SimpleActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.SimpleActivity" - android:theme="@style/CutoutShortEdges" android:label="SimpleActivity" android:exported="true"> <intent-filter> @@ -49,7 +48,6 @@ </activity> <activity android:name=".ImeActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivity" - android:theme="@style/CutoutShortEdges" android:label="ImeActivity" android:exported="true"> <intent-filter> @@ -58,7 +56,6 @@ </intent-filter> </activity> <activity android:name=".ImeActivityAutoFocus" - android:theme="@style/CutoutShortEdges" android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivityAutoFocus" android:windowSoftInputMode="stateVisible" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" @@ -81,7 +78,6 @@ </activity> <activity android:name=".SeamlessRotationActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.SeamlessRotationActivity" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize" android:showWhenLocked="true" android:label="SeamlessActivity" @@ -92,7 +88,6 @@ </intent-filter> </activity> <activity android:name=".NonResizeableActivity" - android:theme="@style/CutoutShortEdges" android:resizeableActivity="false" android:taskAffinity="com.android.server.wm.flicker.testapp.NonResizeableActivity" android:label="NonResizeableActivity" @@ -174,7 +169,6 @@ </activity> <activity android:name=".LaunchNewActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchNewActivity" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize" android:label="LaunchNewActivity" android:exported="true"> @@ -185,7 +179,6 @@ </activity> <activity android:name=".LaunchNewTaskActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchNewTaskActivity" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize" android:label="LaunchNewTaskActivity" android:exported="true"> @@ -207,7 +200,6 @@ </activity> <activity android:name=".PortraitOnlyActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.PortraitOnlyActivity" - android:theme="@style/CutoutShortEdges" android:screenOrientation="portrait" android:configChanges="orientation|screenSize" android:exported="true"> @@ -219,7 +211,6 @@ <activity android:name=".ImeEditorPopupDialogActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.ImeEditorPopupDialogActivity" android:configChanges="orientation|screenSize" - android:theme="@style/CutoutShortEdges" android:label="ImeEditorPopupDialogActivity" android:exported="true"> <intent-filter> @@ -229,7 +220,6 @@ </activity> <activity android:name=".ShowWhenLockedActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.ShowWhenLockedActivity" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize" android:label="ShowWhenLockedActivity" android:showWhenLocked="true" @@ -241,7 +231,6 @@ </activity> <activity android:name=".NotificationActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.NotificationActivity" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize" android:label="NotificationActivity" android:exported="true"> @@ -254,7 +243,6 @@ android:name=".ActivityEmbeddingMainActivity" android:label="ActivityEmbedding Main" android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="true"> <intent-filter> @@ -266,7 +254,6 @@ android:name=".ActivityEmbeddingTrampolineActivity" android:label="ActivityEmbedding Trampoline" android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="false"> </activity> @@ -274,7 +261,6 @@ android:name=".ActivityEmbeddingSecondaryActivity" android:label="ActivityEmbedding Secondary" android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:supportsPictureInPicture="true" android:exported="false"/> @@ -282,21 +268,18 @@ android:name=".ActivityEmbeddingThirdActivity" android:label="ActivityEmbedding Third" android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="false"/> <activity android:name=".ActivityEmbeddingAlwaysExpandActivity" android:label="ActivityEmbedding AlwaysExpand" android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="false"/> <activity android:name=".ActivityEmbeddingPlaceholderPrimaryActivity" android:label="ActivityEmbedding Placeholder Primary" android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="false"> </activity> @@ -304,7 +287,6 @@ android:name=".ActivityEmbeddingPlaceholderSecondaryActivity" android:label="ActivityEmbedding Placeholder Secondary" android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding" - android:theme="@style/CutoutShortEdges" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout" android:exported="false"/> <activity android:name=".MailActivity" @@ -334,7 +316,6 @@ android:supportsPictureInPicture="true" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:taskAffinity="com.android.server.wm.flicker.testapp.PipActivity" - android:theme="@style/CutoutShortEdges" android:launchMode="singleTop" android:label="PipActivity" android:exported="true"> @@ -350,7 +331,6 @@ <activity android:name=".BottomHalfPipLaunchingActivity" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:taskAffinity="com.android.server.wm.flicker.testapp.BottomHalfPipLaunchingActivity" - android:theme="@style/CutoutShortEdges" android:label="BottomHalfPipLaunchingActivity" android:exported="true"> <intent-filter> @@ -371,7 +351,6 @@ <activity android:name=".SplitScreenActivity" android:resizeableActivity="true" android:taskAffinity="com.android.server.wm.flicker.testapp.SplitScreenActivity" - android:theme="@style/CutoutShortEdges" android:label="SplitScreenPrimaryActivity" android:exported="true" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"> @@ -383,7 +362,6 @@ <activity android:name=".SplitScreenSecondaryActivity" android:resizeableActivity="true" android:taskAffinity="com.android.server.wm.flicker.testapp.SplitScreenSecondaryActivity" - android:theme="@style/CutoutShortEdges" android:label="SplitScreenSecondaryActivity" android:exported="true" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"> @@ -396,7 +374,6 @@ </activity> <activity android:name=".SendNotificationActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.SendNotificationActivity" - android:theme="@style/CutoutShortEdges" android:label="SendNotificationActivity" android:exported="true"> <intent-filter> @@ -408,7 +385,6 @@ android:name=".LaunchBubbleActivity" android:label="LaunchBubbleActivity" android:exported="true" - android:theme="@style/CutoutShortEdges" android:launchMode="singleTop"> <intent-filter> <action android:name="android.intent.action.MAIN"/> @@ -420,7 +396,6 @@ android:name=".BubbleActivity" android:label="BubbleActivity" android:exported="false" - android:theme="@style/CutoutShortEdges" android:resizeableActivity="true"/> <activity android:name=".TransferSplashscreenActivity" @@ -468,4 +443,4 @@ </service> </application> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/> -</manifest> +</manifest>
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bottom_half_pip.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bottom_half_pip.xml index 2f9c3aa82057..15e2a798b2cf 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bottom_half_pip.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bottom_half_pip.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical" android:background="@android:color/holo_blue_bright"> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bubble.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bubble.xml index 7c7b2cacaefb..4bc70996eba8 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bubble.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bubble.xml @@ -16,6 +16,7 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:fitsSystemWindows="true" android:layout_width="match_parent" android:layout_height="match_parent"> <Button diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml index f0dfdfce035f..e98ffd045d3d 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml @@ -19,5 +19,6 @@ android:id="@+id/root_activity_layout" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical"> </LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml index 939ba81a47ea..16c906d9ee42 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="@android:color/holo_orange_light"> <LinearLayout diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml index 6d4de995bd73..eedb910b2b4e 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml @@ -19,6 +19,7 @@ android:id="@+id/secondary_activity_layout" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical"> <Button @@ -49,4 +50,4 @@ android:layout_height="48dp" android:text="Enter pip" /> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml index 507c1b622613..5e5203cb0545 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml @@ -17,6 +17,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/holo_green_light" + android:fitsSystemWindows="true" android:focusableInTouchMode="true" android:orientation="vertical"> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_launch_new.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_launch_new.xml index fe7bced690f9..2ab5fe7b9d6c 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_launch_new.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_launch_new.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="@android:color/holo_orange_light"> <Button android:id="@+id/launch_second_activity" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_mail.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_mail.xml index 8a97433ede04..fcef791afe2e 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_mail.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_mail.xml @@ -19,6 +19,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:fitsSystemWindows="true" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_non_resizeable.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_non_resizeable.xml index 6d5a9dd29248..aeb8423680a3 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_non_resizeable.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_non_resizeable.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical" android:background="@android:color/holo_orange_light"> @@ -29,4 +30,4 @@ android:text="NonResizeableActivity" android:textAppearance="?android:attr/textAppearanceLarge"/> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml index 365a0ea017f6..d66b3d74e114 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical" android:background="@android:color/holo_blue_bright"> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_simple.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_simple.xml index 5d94e5177dcc..42213d6812da 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_simple.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_simple.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="@android:color/holo_orange_light"> </LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen.xml index 79e88e49b99e..16c3bc07d55c 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical" android:background="@android:color/holo_green_light"> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml index ed9feaf1eade..7643cf5de216 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_splitscreen_secondary.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical" android:background="@android:color/holo_blue_light"> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml index c34d2003ef42..79e7bb5de96e 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:gravity="center" android:orientation="vertical" android:background="@android:color/holo_orange_light"> @@ -39,4 +40,4 @@ android:gravity="center_vertical|center_horizontal" android:text="Start Media Projection with extra intent" android:textAppearance="?android:attr/textAppearanceLarge"/> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_surfaceview.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_surfaceview.xml index 0b4693dec6e1..4d12168ca8db 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_surfaceview.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_surfaceview.xml @@ -19,6 +19,7 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="@android:color/holo_orange_light"> <SurfaceView diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml index 0730ded66ce4..32df5f015414 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml @@ -15,6 +15,7 @@ ~ limitations under the License. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:fitsSystemWindows="true" android:layout_width="match_parent" android:layout_height="match_parent"> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml index ff4ead95f16e..a0c87fd17fc3 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml @@ -17,6 +17,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical" android:background="@android:color/black"> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/notification_button.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/notification_button.xml index 78072006f681..da58b3f43c73 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/notification_button.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/notification_button.xml @@ -18,10 +18,12 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="@android:color/holo_orange_light"> <Button android:id="@+id/post_notification" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_gravity="center_horizontal|center_vertical" android:text="Post Notification" /> </LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/task_button.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/task_button.xml index 8f75d175d00a..ec3135c0a42d 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/task_button.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/task_button.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="@android:color/holo_orange_light"> <Button android:id="@+id/launch_new_task" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml index 837d050b73ff..ca3244820893 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml @@ -36,10 +36,6 @@ <item name="android:windowLayoutInDisplayCutoutMode">default</item> </style> - <style name="CutoutShortEdges" parent="@style/DefaultTheme"> - <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> - </style> - <style name="CutoutNever" parent="@style/DefaultTheme"> <item name="android:windowLayoutInDisplayCutoutMode">never</item> </style> @@ -78,4 +74,4 @@ <!-- Here we want to match the duration of our AVD --> <item name="android:windowSplashScreenAnimationDuration">900</item> </style> -</resources> +</resources>
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java index 4418b5a5ff82..30bf616cfe74 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java @@ -30,7 +30,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.util.Log; -import android.view.WindowManager; public class ImeActivity extends Activity { @@ -64,10 +63,6 @@ public class ImeActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); setContentView(R.layout.activity_ime); final var filter = new IntentFilter(); diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeEditorPopupDialogActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeEditorPopupDialogActivity.java index 95f933f97bb2..887a15c9ea90 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeEditorPopupDialogActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeEditorPopupDialogActivity.java @@ -30,8 +30,6 @@ public class ImeEditorPopupDialogActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; p.softInputMode = SOFT_INPUT_STATE_ALWAYS_HIDDEN; getWindow().setAttributes(p); LinearLayout layout = new LinearLayout(this); diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewActivity.java index e5710c850f07..97d7a64d5ebf 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewActivity.java @@ -19,17 +19,12 @@ package com.android.server.wm.flicker.testapp; import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.view.WindowManager; import android.widget.Button; public class LaunchNewActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); setContentView(R.layout.activity_launch_new); Button button = findViewById(R.id.launch_second_activity); diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewTaskActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewTaskActivity.java index 1809781b33e6..402a393e7028 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewTaskActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchNewTaskActivity.java @@ -21,17 +21,12 @@ import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.view.WindowManager; import android.widget.Button; public class LaunchNewTaskActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); setContentView(R.layout.task_button); Button button = findViewById(R.id.launch_new_task); diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NotificationActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NotificationActivity.java index d6427abcc65a..61254385e980 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NotificationActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NotificationActivity.java @@ -45,14 +45,10 @@ public class NotificationActivity extends Activity { requestPermissions(new String[] { POST_NOTIFICATIONS }, 0); } - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); setContentView(R.layout.notification_button); - Button button = findViewById(R.id.post_notification); - button.setOnClickListener(v -> postNotification()); + ((Button) findViewById(R.id.post_notification)) + .setOnClickListener(v -> postNotification()); createNotificationChannel(); } diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java index ee25ab2fb66c..e030dcf0db9b 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java @@ -47,8 +47,6 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.Rational; import android.view.View; -import android.view.Window; -import android.view.WindowManager; import android.widget.CheckBox; import android.widget.RadioButton; @@ -145,12 +143,6 @@ public class PipActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final Window window = getWindow(); - final WindowManager.LayoutParams layoutParams = window.getAttributes(); - layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - window.setAttributes(layoutParams); - setContentView(R.layout.activity_pip); findViewById(R.id.media_session_start) diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitOnlyActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitOnlyActivity.java index b1876b5e5511..552d843676bb 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitOnlyActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitOnlyActivity.java @@ -18,16 +18,11 @@ package com.android.server.wm.flicker.testapp; import android.app.Activity; import android.os.Bundle; -import android.view.WindowManager; public class PortraitOnlyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); setContentView(R.layout.activity_simple); } } diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java index ce7a0059fa2d..e98c34db0cd9 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SeamlessRotationActivity.java @@ -61,8 +61,6 @@ public class SeamlessRotationActivity extends Activity { private void enableSeamlessRotation() { WindowManager.LayoutParams p = getWindow().getAttributes(); p.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ShowWhenLockedActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ShowWhenLockedActivity.java index 6f94b74ccf41..a533c9052574 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ShowWhenLockedActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ShowWhenLockedActivity.java @@ -18,16 +18,11 @@ package com.android.server.wm.flicker.testapp; import android.app.Activity; import android.os.Bundle; -import android.view.WindowManager; public class ShowWhenLockedActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); setContentView(R.layout.activity_simple); } } diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SimpleActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SimpleActivity.java index 699abf87d341..c56eefe32189 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SimpleActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/SimpleActivity.java @@ -18,16 +18,11 @@ package com.android.server.wm.flicker.testapp; import android.app.Activity; import android.os.Bundle; -import android.view.WindowManager; public class SimpleActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - WindowManager.LayoutParams p = getWindow().getAttributes(); - p.layoutInDisplayCutoutMode = WindowManager.LayoutParams - .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - getWindow().setAttributes(p); setContentView(R.layout.activity_simple); } } diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index c666fb7e05f1..2799f6c79215 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -382,14 +382,6 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "META + DEL -> Back", - intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_DEL), - KeyGestureEvent.KEY_GESTURE_TYPE_BACK, - intArrayOf(KeyEvent.KEYCODE_DEL), - KeyEvent.META_META_ON, - intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) - ), - TestData( "META + ESC -> Back", intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ESCAPE), KeyGestureEvent.KEY_GESTURE_TYPE_BACK, diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp index 16c6e3baf051..46cb5b7482e9 100644 --- a/tests/PackageWatchdog/Android.bp +++ b/tests/PackageWatchdog/Android.bp @@ -55,4 +55,8 @@ android_test { "mts-crashrecovery", ], min_sdk_version: "36", + + // Test coverage system runs on different devices. Need to + // compile for all architecture. + compile_multilib: "both", } diff --git a/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java b/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java index 51d57f0a0de9..f5d4b0c5e345 100644 --- a/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java +++ b/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java @@ -24,15 +24,20 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.content.Context; +import android.hardware.usb.IUsbManagerInternal; import android.hardware.usb.IUsbOperationInternal; import android.hardware.usb.flags.Flags; import android.hardware.usb.UsbPort; @@ -70,7 +75,9 @@ public class UsbServiceTest { @Mock private IUsbOperationInternal mCallback; - private static final String TEST_PORT_ID = "123"; + private static final String TEST_PORT_ID = "1"; + + private static final String TEST_PORT_ID_2 = "2"; private static final int TEST_TRANSACTION_ID = 1; @@ -84,7 +91,7 @@ public class UsbServiceTest { private UsbService mUsbService; - private UsbManagerInternal mUsbManagerInternal; + private IUsbManagerInternal mIUsbManagerInternal; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -101,9 +108,9 @@ public class UsbServiceTest { mUsbService = new UsbService(mContext, mUsbPortManager, mUsbAlsaManager, mUserManager, mUsbSettingsManager); - mUsbManagerInternal = LocalServices.getService(UsbManagerInternal.class); - assertWithMessage("LocalServices.getService(UsbManagerInternal.class)") - .that(mUsbManagerInternal).isNotNull(); + mIUsbManagerInternal = LocalServices.getService(IUsbManagerInternal.class); + assertWithMessage("LocalServices.getService(IUsbManagerInternal.class)") + .that(mIUsbManagerInternal).isNotNull(); } private void assertToggleUsbSuccessfully(int requester, boolean enable, @@ -255,30 +262,42 @@ public class UsbServiceTest { assertToggleUsbSuccessfully(TEST_INTERNAL_REQUESTER_REASON_1, true, false); } - /** - * Verify USB Manager internal calls mPortManager to get UsbPorts - */ @Test - public void usbManagerInternal_getPorts_callsPortManager() { - when(mUsbPortManager.getPorts()).thenReturn(new UsbPort[] {}); - - UsbPort[] ports = mUsbManagerInternal.getPorts(); - - verify(mUsbPortManager).getPorts(); - assertEquals(ports.length, 0); + public void usbManagerInternal_enableUsbDataSignal_successfullyEnabled() { + assertTrue(runInternalUsbDataSignalTest(true, true, true)); } @Test - public void usbManagerInternal_enableUsbData_successfullyEnable() { - boolean desiredEnableState = true; + public void usbManagerInternal_enableUsbDataSignal_successfullyDisabled() { + assertTrue(runInternalUsbDataSignalTest(false, true, true)); + } - assertTrue(mUsbManagerInternal.enableUsbData(TEST_PORT_ID, desiredEnableState, - TEST_TRANSACTION_ID, mCallback, TEST_INTERNAL_REQUESTER_REASON_1)); + @Test + public void usbManagerInternal_enableUsbDataSignal_returnsFalseIfOnePortFails() { + assertFalse(runInternalUsbDataSignalTest(true, true, false)); + } - verify(mUsbPortManager).enableUsbData(TEST_PORT_ID, - desiredEnableState, TEST_TRANSACTION_ID, mCallback, null); - verifyZeroInteractions(mCallback); - clearInvocations(mUsbPortManager); - clearInvocations(mCallback); + private boolean runInternalUsbDataSignalTest(boolean desiredEnableState, boolean portOneSuccess, + boolean portTwoSuccess) { + UsbPort port = mock(UsbPort.class); + UsbPort port2 = mock(UsbPort.class); + when(port.getId()).thenReturn(TEST_PORT_ID); + when(port2.getId()).thenReturn(TEST_PORT_ID_2); + when(mUsbPortManager.getPorts()).thenReturn(new UsbPort[] { port, port2 }); + when(mUsbPortManager.enableUsbData(eq(TEST_PORT_ID), + eq(desiredEnableState), anyInt(), any(IUsbOperationInternal.class), isNull())) + .thenReturn(portOneSuccess); + when(mUsbPortManager.enableUsbData(eq(TEST_PORT_ID_2), + eq(desiredEnableState), anyInt(), any(IUsbOperationInternal.class), isNull())) + .thenReturn(portTwoSuccess); + try { + boolean result = mIUsbManagerInternal.enableUsbDataSignal(desiredEnableState, + TEST_INTERNAL_REQUESTER_REASON_1); + clearInvocations(mUsbPortManager); + return result; + } catch(RemoteException e) { + fail("RemoteException thrown when calling enableUsbDataSignal"); + return false; + } } } diff --git a/tools/fonts/Android.bp b/tools/fonts/Android.bp index f8629f9bd0b8..07caa9a979d9 100644 --- a/tools/fonts/Android.bp +++ b/tools/fonts/Android.bp @@ -23,11 +23,6 @@ package { python_defaults { name: "fonts_python_defaults", - version: { - py3: { - embedded_launcher: true, - }, - }, } python_binary_host { diff --git a/tools/lint/fix/Android.bp b/tools/lint/fix/Android.bp index ddacf57c3a3e..43f21221ae5a 100644 --- a/tools/lint/fix/Android.bp +++ b/tools/lint/fix/Android.bp @@ -25,11 +25,6 @@ python_binary_host { name: "lint_fix", main: "soong_lint_fix.py", srcs: ["soong_lint_fix.py"], - version: { - py3: { - embedded_launcher: true, - }, - }, } python_library_host { diff --git a/tools/lint/global/integration_tests/Android.bp b/tools/lint/global/integration_tests/Android.bp index 05ba405d2c52..f88709375c98 100644 --- a/tools/lint/global/integration_tests/Android.bp +++ b/tools/lint/global/integration_tests/Android.bp @@ -65,9 +65,4 @@ python_test_host { "AndroidGlobalLintTestNoAidl_py", "AndroidGlobalLintTestMissingAnnotation_py", ], - version: { - py3: { - embedded_launcher: true, - }, - }, } |