diff options
141 files changed, 3105 insertions, 1160 deletions
diff --git a/apct-tests/perftests/tracing/Android.bp b/apct-tests/perftests/tracing/Android.bp index 08e365be514a..8814216644d7 100644 --- a/apct-tests/perftests/tracing/Android.bp +++ b/apct-tests/perftests/tracing/Android.bp @@ -22,6 +22,7 @@ android_test { "apct-perftests-utils", "collector-device-lib", "platform-test-annotations", + "perfetto_trace_java_protos", ], test_suites: [ "device-tests", diff --git a/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java b/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java index f4759b8bd35c..7ef8c53d1d62 100644 --- a/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java +++ b/apct-tests/perftests/tracing/src/com/android/internal/protolog/ProtoLogPerfTest.java @@ -17,10 +17,12 @@ package com.android.internal.protolog; import android.app.Activity; import android.os.Bundle; +import android.os.ServiceManager.ServiceNotFoundException; import android.perftests.utils.Stats; import androidx.test.InstrumentationRegistry; +import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogLevel; @@ -31,6 +33,8 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import perfetto.protos.ProtologCommon; + import java.util.ArrayList; import java.util.Collection; @@ -65,24 +69,48 @@ public class ProtoLogPerfTest { }; } + private IProtoLog mProcessedProtoLogger; + private static final String MOCK_TEST_FILE_PATH = "mock/file/path"; + private static final perfetto.protos.Protolog.ProtoLogViewerConfig VIEWER_CONFIG = + perfetto.protos.Protolog.ProtoLogViewerConfig.newBuilder() + .addGroups( + perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder() + .setId(1) + .setName(TestProtoLogGroup.TEST_GROUP.toString()) + .setTag(TestProtoLogGroup.TEST_GROUP.getTag()) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(123) + .setMessage("My Test Debug Log Message %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG) + .setGroupId(1) + .setLocation("com/test/MyTestClass.java:123") + ).build(); + @BeforeClass public static void init() { ProtoLog.init(TestProtoLogGroup.values()); } @Before - public void setUp() { + public void setUp() throws ServiceNotFoundException { TestProtoLogGroup.TEST_GROUP.setLogToProto(mLogToProto); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(mLogToLogcat); + + mProcessedProtoLogger = new ProcessedPerfettoProtoLogImpl( + MOCK_TEST_FILE_PATH, + () -> new AutoClosableProtoInputStream(VIEWER_CONFIG.toByteArray()), + () -> {}, + TestProtoLogGroup.values() + ); } @Test public void log_Processed_NoArgs() { - final var protoLog = ProtoLog.getSingleInstance(); final var perfMonitor = new PerfMonitor(); while (perfMonitor.keepRunning()) { - protoLog.log( + mProcessedProtoLogger.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123, 0, (Object[]) null); } @@ -90,11 +118,10 @@ public class ProtoLogPerfTest { @Test public void log_Processed_WithArgs() { - final var protoLog = ProtoLog.getSingleInstance(); final var perfMonitor = new PerfMonitor(); while (perfMonitor.keepRunning()) { - protoLog.log( + mProcessedProtoLogger.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123, 0b1110101001010100, new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index e7f4dbc24022..b0a8b1b2dbf3 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -237,6 +237,7 @@ import com.android.internal.os.DebugStore; import com.android.internal.os.RuntimeInit; import com.android.internal.os.SafeZipPathValidatorCallback; import com.android.internal.os.SomeArgs; +import com.android.internal.os.logging.MetricsLoggerWrapper; import com.android.internal.policy.DecorView; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; @@ -2680,7 +2681,10 @@ public final class ActivityThread extends ClientTransactionHandler handleUnstableProviderDied((IBinder)msg.obj, false); break; case REQUEST_ASSIST_CONTEXT_EXTRAS: + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, + "handleRequestAssistContextExtras"); handleRequestAssistContextExtras((RequestAssistContextExtras)msg.obj); + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; case TRANSLUCENT_CONVERSION_COMPLETE: handleTranslucentConversionComplete((IBinder)msg.obj, msg.arg1 == 1); @@ -7675,6 +7679,16 @@ public final class ActivityThread extends ClientTransactionHandler } } }); + + // Register callback to report native memory metrics post GC cleanup + if (Flags.reportPostgcMemoryMetrics() && + com.android.libcore.readonly.Flags.postCleanupApis()) { + VMRuntime.addPostCleanupCallback(new Runnable() { + @Override public void run() { + MetricsLoggerWrapper.logPostGcMemorySnapshot(); + } + }); + } } @UnsupportedAppUsage diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index e882bb564db9..081ce31e0886 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -345,6 +345,15 @@ public class TaskInfo { */ public AppCompatTaskInfo appCompatTaskInfo = AppCompatTaskInfo.create(); + /** + * The top activity's main window frame if it doesn't match the top activity bounds. + * {@code null}, otherwise. + * + * @hide + */ + @Nullable + public Rect topActivityMainWindowFrame; + TaskInfo() { // Do nothing } @@ -477,7 +486,8 @@ public class TaskInfo { && Objects.equals(capturedLink, that.capturedLink) && capturedLinkTimestamp == that.capturedLinkTimestamp && requestedVisibleTypes == that.requestedVisibleTypes - && appCompatTaskInfo.equalsForTaskOrganizer(that.appCompatTaskInfo); + && appCompatTaskInfo.equalsForTaskOrganizer(that.appCompatTaskInfo) + && Objects.equals(topActivityMainWindowFrame, that.topActivityMainWindowFrame); } /** @@ -553,6 +563,7 @@ public class TaskInfo { capturedLinkTimestamp = source.readLong(); requestedVisibleTypes = source.readInt(); appCompatTaskInfo = source.readTypedObject(AppCompatTaskInfo.CREATOR); + topActivityMainWindowFrame = source.readTypedObject(Rect.CREATOR); } /** @@ -606,6 +617,7 @@ public class TaskInfo { dest.writeLong(capturedLinkTimestamp); dest.writeInt(requestedVisibleTypes); dest.writeTypedObject(appCompatTaskInfo, flags); + dest.writeTypedObject(topActivityMainWindowFrame, flags); } @Override @@ -649,6 +661,7 @@ public class TaskInfo { + " capturedLinkTimestamp=" + capturedLinkTimestamp + " requestedVisibleTypes=" + requestedVisibleTypes + " appCompatTaskInfo=" + appCompatTaskInfo + + " topActivityMainWindowFrame=" + topActivityMainWindowFrame + "}"; } } diff --git a/core/java/android/app/metrics.aconfig b/core/java/android/app/metrics.aconfig new file mode 100644 index 000000000000..488f1c71990b --- /dev/null +++ b/core/java/android/app/metrics.aconfig @@ -0,0 +1,10 @@ +package: "android.app" +container: "system" + +flag { + namespace: "system_performance" + name: "report_postgc_memory_metrics" + is_exported: false + description: "Controls whether to report memory metrics post GC cleanup" + bug: "331243037" +} diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index 047d1fa4f49a..26ffa11823d8 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -39,3 +39,11 @@ flag { description: "This flag controls whether LSKF fallback is removed from biometric prompt when the phone is outside trusted locations" bug: "322081563" } + +flag { + name: "screen_off_unlock_udfps" + is_exported: true + namespace: "biometrics_integration" + description: "This flag controls Whether to enable fp unlock when screen turns off on udfps devices" + bug: "373792870" +} diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java index 47541ca16cda..59a602ca092d 100644 --- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java +++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java @@ -18,6 +18,7 @@ package android.hardware.display; import android.annotation.TestApi; import android.content.Context; +import android.hardware.biometrics.Flags; import android.os.Build; import android.os.SystemProperties; import android.provider.Settings; @@ -41,6 +42,7 @@ public class AmbientDisplayConfiguration { private final Context mContext; private final boolean mAlwaysOnByDefault; private final boolean mPickupGestureEnabledByDefault; + private final boolean mScreenOffUdfpsEnabledByDefault; /** Copied from android.provider.Settings.Secure since these keys are hidden. */ private static final String[] DOZE_SETTINGS = { @@ -68,6 +70,8 @@ public class AmbientDisplayConfiguration { mAlwaysOnByDefault = mContext.getResources().getBoolean(R.bool.config_dozeAlwaysOnEnabled); mPickupGestureEnabledByDefault = mContext.getResources().getBoolean(R.bool.config_dozePickupGestureEnabled); + mScreenOffUdfpsEnabledByDefault = + mContext.getResources().getBoolean(R.bool.config_screen_off_udfps_enabled); } /** @hide */ @@ -146,7 +150,9 @@ public class AmbientDisplayConfiguration { /** @hide */ public boolean screenOffUdfpsEnabled(int user) { return !TextUtils.isEmpty(udfpsLongPressSensorType()) - && boolSettingDefaultOff("screen_off_udfps_enabled", user); + && ((mScreenOffUdfpsEnabledByDefault && Flags.screenOffUnlockUdfps()) + ? boolSettingDefaultOn("screen_off_udfps_enabled", user) + : boolSettingDefaultOff("screen_off_udfps_enabled", user)); } /** @hide */ diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 5ee61bcd436a..2df541818e3d 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -99,6 +99,7 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59; public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60; public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61; + public static final int KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY = 62; public static final int FLAG_CANCELLED = 1; @@ -175,7 +176,7 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT, KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS, - + KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyGestureType { @@ -415,6 +416,8 @@ public final class KeyGestureEvent { case KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT: case KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT: return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__CHANGE_SPLITSCREEN_FOCUS; + case KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY: + return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MOVE_TO_NEXT_DISPLAY; case KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT: return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TRIGGER_BUG_REPORT; case KEY_GESTURE_TYPE_LOCK_SCREEN: @@ -530,6 +533,8 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT"; case KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT: return "KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT"; + case KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY: + return "KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY"; case KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT: return "KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT"; case KEY_GESTURE_TYPE_LOCK_SCREEN: diff --git a/core/java/android/os/VibratorInfo.java b/core/java/android/os/VibratorInfo.java index 9419032c46f8..9dec8673f019 100644 --- a/core/java/android/os/VibratorInfo.java +++ b/core/java/android/os/VibratorInfo.java @@ -316,9 +316,7 @@ public class VibratorInfo implements Parcelable { * @return True if the hardware can control the frequency of the vibrations, otherwise false. */ public boolean hasFrequencyControl() { - // We currently can only control frequency of the vibration using the compose PWLE method. - return hasCapability( - IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS); + return hasCapability(IVibrator.CAP_FREQUENCY_CONTROL); } /** @@ -481,7 +479,8 @@ public class VibratorInfo implements Parcelable { * @return True if the hardware supports creating envelope effects, false otherwise. */ public boolean areEnvelopeEffectsSupported() { - return hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + return hasCapability( + IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); } /** diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index e8ef9d65a2b4..bce51f297aff 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -1701,6 +1701,11 @@ public class PhoneStateListener { public final void onCarrierRoamingNtnEligibleStateChanged(boolean eligible) { // not supported on the deprecated interface - Use TelephonyCallback instead } + + public final void onCarrierRoamingNtnAvailableServicesChanged( + @NetworkRegistrationInfo.ServiceType int[] availableServices) { + // not supported on the deprecated interface - Use TelephonyCallback instead + } } private void log(String s) { diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java index 5295b606dd19..46e27dc60adc 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -681,6 +681,20 @@ public class TelephonyCallback { public static final int EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED = 43; /** + * Event for listening to changes in carrier roaming non-terrestrial network available services + * via callback onCarrierRoamingNtnAvailableServicesChanged(). + * This callback is triggered when the available services provided by the carrier roaming + * satellite changes. The carrier roaming satellite is defined by the following conditions. + * <ul> + * <li>Subscription supports attaching to satellite which is defined by + * {@link CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} </li> + * </ul> + * + * @hide + */ + public static final int EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED = 44; + + /** * @hide */ @IntDef(prefix = {"EVENT_"}, value = { @@ -726,7 +740,8 @@ public class TelephonyCallback { EVENT_EMERGENCY_CALLBACK_MODE_CHANGED, EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED, EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED, - EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED + EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED, + EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface TelephonyEvent { @@ -1784,6 +1799,15 @@ public class TelephonyCallback { * </ul> */ default void onCarrierRoamingNtnEligibleStateChanged(boolean eligible) {} + + /** + * Callback invoked when carrier roaming non-terrestrial network available + * service changes. + * + * @param availableServices The list of the supported services. + */ + default void onCarrierRoamingNtnAvailableServicesChanged( + @NetworkRegistrationInfo.ServiceType List<Integer> availableServices) {} } /** @@ -2235,5 +2259,19 @@ public class TelephonyCallback { Binder.withCleanCallingIdentity(() -> mExecutor.execute( () -> listener.onCarrierRoamingNtnEligibleStateChanged(eligible))); } + + public void onCarrierRoamingNtnAvailableServicesChanged( + @NetworkRegistrationInfo.ServiceType int[] availableServices) { + if (!Flags.carrierRoamingNbIotNtn()) return; + + CarrierRoamingNtnModeListener listener = + (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get(); + if (listener == null) return; + + List<Integer> ServiceList = Arrays.stream(availableServices).boxed() + .collect(Collectors.toList()); + Binder.withCleanCallingIdentity(() -> mExecutor.execute( + () -> listener.onCarrierRoamingNtnAvailableServicesChanged(ServiceList))); + } } } diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 3c7e924f07df..4d50a450490e 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -1118,6 +1118,21 @@ public class TelephonyRegistryManager { } /** + * Notify external listeners that carrier roaming non-terrestrial available services changed. + * @param availableServices The list of the supported services. + * @hide + */ + public void notifyCarrierRoamingNtnAvailableServicesChanged( + int subId, @NetworkRegistrationInfo.ServiceType int[] availableServices) { + try { + sRegistry.notifyCarrierRoamingNtnAvailableServicesChanged(subId, availableServices); + } catch (RemoteException ex) { + // system server crash + throw ex.rethrowFromSystemServer(); + } + } + + /** * Processes potential event changes from the provided {@link TelephonyCallback}. * * @param telephonyCallback callback for monitoring callback changes to the telephony state. @@ -1272,12 +1287,9 @@ public class TelephonyRegistryManager { if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnModeListener) { eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED); - } - - if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnModeListener) { eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_ELIGIBLE_STATE_CHANGED); + eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED); } - return eventList; } diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index 6448f10f01b9..003393c337e7 100644 --- a/core/java/com/android/internal/jank/FrameTracker.java +++ b/core/java/com/android/internal/jank/FrameTracker.java @@ -230,7 +230,7 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai mRendererWrapper = mSurfaceOnly ? null : renderer; mMetricsWrapper = mSurfaceOnly ? null : metrics; mViewRoot = mSurfaceOnly ? null : viewRootWrapper; - mObserver = mSurfaceOnly + mObserver = mSurfaceOnly || (Flags.useSfFrameDuration() && Flags.ignoreHwuiIsFirstFrame()) ? null : new HardwareRendererObserver(this, mMetricsWrapper.getTiming(), mHandler, /* waitForPresentTime= */ false); @@ -253,6 +253,7 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() { @Override public void surfaceCreated(SurfaceControl.Transaction t) { + Trace.beginSection("FrameTracker#surfaceCreated"); mHandler.runWithScissors(() -> { if (mSurfaceControl == null) { mSurfaceControl = mViewRoot.getSurfaceControl(); @@ -262,6 +263,7 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai } } }, EXECUTOR_TASK_TIMEOUT); + Trace.endSection(); } @Override @@ -464,23 +466,28 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai @Override public void onJankDataAvailable(SurfaceControl.JankData[] jankData) { postCallback(() -> { - if (mCancelled || mMetricsFinalized) { - return; - } - - for (SurfaceControl.JankData jankStat : jankData) { - if (!isInRange(jankStat.frameVsyncId)) { - continue; + try { + Trace.beginSection("FrameTracker#onJankDataAvailable"); + if (mCancelled || mMetricsFinalized) { + return; } - JankInfo info = findJankInfo(jankStat.frameVsyncId); - if (info != null) { - info.update(jankStat); - } else { - mJankInfos.put((int) jankStat.frameVsyncId, - JankInfo.createFromSurfaceControlCallback(jankStat)); + + for (SurfaceControl.JankData jankStat : jankData) { + if (!isInRange(jankStat.frameVsyncId)) { + continue; + } + JankInfo info = findJankInfo(jankStat.frameVsyncId); + if (info != null) { + info.update(jankStat); + } else { + mJankInfos.put((int) jankStat.frameVsyncId, + JankInfo.createFromSurfaceControlCallback(jankStat)); + } } + processJankInfos(); + } finally { + Trace.endSection(); } - processJankInfos(); }); } @@ -507,29 +514,35 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai @Override public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) { postCallback(() -> { - if (mCancelled || mMetricsFinalized) { - return; - } - - // Since this callback might come a little bit late after the end() call. - // We should keep tracking the begin / end timestamp that we can compare with - // vsync timestamp to check if the frame is in the duration of the CUJ. - long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION); - boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1; - long frameVsyncId = - mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID]; + try { + Trace.beginSection("FrameTracker#onFrameMetricsAvailable"); + if (mCancelled || mMetricsFinalized) { + return; + } - if (!isInRange(frameVsyncId)) { - return; - } - JankInfo info = findJankInfo(frameVsyncId); - if (info != null) { - info.update(totalDurationNanos, isFirstFrame); - } else { - mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback( - frameVsyncId, totalDurationNanos, isFirstFrame)); + // Since this callback might come a little bit late after the end() call. + // We should keep tracking the begin / end timestamp that we can compare with + // vsync timestamp to check if the frame is in the duration of the CUJ. + long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION); + boolean isFirstFrame = + mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1; + long frameVsyncId = + mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID]; + + if (!isInRange(frameVsyncId)) { + return; + } + JankInfo info = findJankInfo(frameVsyncId); + if (info != null) { + info.update(totalDurationNanos, isFirstFrame); + } else { + mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback( + frameVsyncId, totalDurationNanos, isFirstFrame)); + } + processJankInfos(); + } finally { + Trace.endSection(); } - processJankInfos(); }); } @@ -568,13 +581,20 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai } private boolean callbacksReceived(JankInfo info) { - return mSurfaceOnly + return mObserver == null ? info.surfaceControlCallbackFired : info.hwuiCallbackFired && info.surfaceControlCallbackFired; } @UiThread private void finish() { + Trace.beginSection("FrameTracker#finish"); + finishTraced(); + Trace.endSection(); + } + + @UiThread + private void finishTraced() { if (mMetricsFinalized || mCancelled) return; mMetricsFinalized = true; @@ -599,7 +619,7 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai for (int i = 0; i < mJankInfos.size(); i++) { JankInfo info = mJankInfos.valueAt(i); final boolean isFirstDrawn = !mSurfaceOnly && info.isFirstFrame; - if (isFirstDrawn) { + if (isFirstDrawn && !Flags.ignoreHwuiIsFirstFrame()) { continue; } if (info.frameVsyncId > mEndVsyncId) { @@ -636,7 +656,7 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai } // TODO (b/174755489): Early latch currently gets fired way too often, so we have // to ignore it for now. - if (!mSurfaceOnly && !info.hwuiCallbackFired) { + if (mObserver != null && !info.hwuiCallbackFired) { markEvent("FT#MissedHWUICallback", info.frameVsyncId); Log.w(TAG, "Missing HWUI jank callback for vsyncId: " + info.frameVsyncId + ", CUJ=" + name); @@ -762,7 +782,9 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai * @param observer observer */ public void addObserver(HardwareRendererObserver observer) { - mRenderer.addObserver(observer); + if (observer != null) { + mRenderer.addObserver(observer); + } } /** @@ -770,7 +792,9 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai * @param observer observer */ public void removeObserver(HardwareRendererObserver observer) { - mRenderer.removeObserver(observer); + if (observer != null) { + mRenderer.removeObserver(observer); + } } } diff --git a/core/java/com/android/internal/jank/flags.aconfig b/core/java/com/android/internal/jank/flags.aconfig index 82f50ae848b3..287ad1856258 100644 --- a/core/java/com/android/internal/jank/flags.aconfig +++ b/core/java/com/android/internal/jank/flags.aconfig @@ -8,3 +8,10 @@ flag { bug: "354763298" is_fixed_read_only: true } +flag { + name: "ignore_hwui_is_first_frame" + namespace: "window_surfaces" + description: "Whether to remove the usage of the HWUI 'is first frame' flag to ignore jank" + bug: "354763298" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java b/core/java/com/android/internal/os/ProcfsMemoryUtil.java index 6cb6dc07f8b8..382f6c4a8a16 100644 --- a/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java +++ b/core/java/com/android/internal/os/ProcfsMemoryUtil.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.server.stats.pull; +package com.android.internal.os; -import static android.os.Process.PROC_OUT_STRING; +import static android.os.Process.*; import android.annotation.Nullable; import android.os.Process; @@ -23,6 +23,7 @@ import android.util.SparseArray; public final class ProcfsMemoryUtil { private static final int[] CMDLINE_OUT = new int[] { PROC_OUT_STRING }; + private static final int[] OOM_SCORE_ADJ_OUT = new int[] { PROC_NEWLINE_TERM | PROC_OUT_LONG }; private static final String[] STATUS_KEYS = new String[] { "Uid:", "VmHWM:", @@ -38,17 +39,34 @@ public final class ProcfsMemoryUtil { private ProcfsMemoryUtil() {} /** - * Reads memory stats of a process from procfs. Returns values of the VmHWM, VmRss, AnonRSS, - * VmSwap, RssShmem fields in /proc/pid/status in kilobytes or null if not available. + * Reads memory stats of a process from procfs. + * + * Returns values of the VmHWM, VmRss, AnonRSS, VmSwap, RssShmem fields in + * /proc/pid/status in kilobytes or null if not available. */ @Nullable public static MemorySnapshot readMemorySnapshotFromProcfs(int pid) { + return readMemorySnapshotFromProcfs("/proc/" + pid + "/status"); + } + + /** + * Reads memory stats of the current process from procfs. + * + * Returns values of the VmHWM, VmRss, AnonRSS, VmSwap, RssShmem fields in + * /proc/self/status in kilobytes or null if not available. + */ + @Nullable + public static MemorySnapshot readMemorySnapshotFromProcfs() { + return readMemorySnapshotFromProcfs("/proc/self/status"); + } + + private static MemorySnapshot readMemorySnapshotFromProcfs(String path) { long[] output = new long[STATUS_KEYS.length]; output[0] = -1; output[3] = -1; output[4] = -1; output[5] = -1; - Process.readProcLines("/proc/" + pid + "/status", STATUS_KEYS, output); + Process.readProcLines(path, STATUS_KEYS, output); if (output[0] == -1 || output[3] == -1 || output[4] == -1 || output[5] == -1) { // Could not open or parse file. return null; @@ -70,14 +88,54 @@ public final class ProcfsMemoryUtil { * if the file is not available. */ public static String readCmdlineFromProcfs(int pid) { + return readCmdlineFromProcfs("/proc/" + pid + "/cmdline"); + } + + /** + * Reads cmdline of the current process from procfs. + * + * Returns content of /proc/pid/cmdline (e.g. /system/bin/statsd) or an empty string + * if the file is not available. + */ + public static String readCmdlineFromProcfs() { + return readCmdlineFromProcfs("/proc/self/cmdline"); + } + + private static String readCmdlineFromProcfs(String path) { String[] cmdline = new String[1]; - if (!Process.readProcFile("/proc/" + pid + "/cmdline", CMDLINE_OUT, cmdline, null, null)) { + if (!Process.readProcFile(path, CMDLINE_OUT, cmdline, null, null)) { return ""; } return cmdline[0]; } /** + * Reads oom_score_adj of a process from procfs + * + * Returns content of /proc/pid/oom_score_adj. Defaults to 0 if reading fails. + */ + public static int readOomScoreAdjFromProcfs(int pid) { + return readOomScoreAdjFromProcfs("/proc/" + pid + "/oom_score_adj"); + } + + /** + * Reads oom_score_adj of the current process from procfs + * + * Returns content of /proc/pid/oom_score_adj. Defaults to 0 if reading fails. + */ + public static int readOomScoreAdjFromProcfs() { + return readOomScoreAdjFromProcfs("/proc/self/oom_score_adj"); + } + + private static int readOomScoreAdjFromProcfs(String path) { + long[] oom_score_adj = new long[1]; + if (Process.readProcFile(path, OOM_SCORE_ADJ_OUT, null, oom_score_adj, null)) { + return (int)oom_score_adj[0]; + } + return 0; + } + + /** * Scans all /proc/pid/cmdline entries and returns a mapping between pid and cmdline. */ public static SparseArray<String> getProcessCmdlines() { @@ -109,7 +167,7 @@ public final class ProcfsMemoryUtil { /** Reads and parses selected entries of /proc/vmstat. */ @Nullable - static VmStat readVmStat() { + public static VmStat readVmStat() { long[] vmstat = new long[VMSTAT_KEYS.length]; vmstat[0] = -1; Process.readProcLines("/proc/vmstat", VMSTAT_KEYS, vmstat); @@ -121,7 +179,7 @@ public final class ProcfsMemoryUtil { return result; } - static final class VmStat { + public static final class VmStat { public int oomKillCount; } } diff --git a/core/java/com/android/internal/os/logging/MetricsLoggerWrapper.java b/core/java/com/android/internal/os/logging/MetricsLoggerWrapper.java index b42ea7d0b769..e2237f61dfbb 100644 --- a/core/java/com/android/internal/os/logging/MetricsLoggerWrapper.java +++ b/core/java/com/android/internal/os/logging/MetricsLoggerWrapper.java @@ -16,9 +16,15 @@ package com.android.internal.os.logging; +import android.app.Application; +import android.os.Process; +import android.util.Log; import android.view.WindowManager.LayoutParams; +import com.android.internal.os.ProcfsMemoryUtil; import com.android.internal.util.FrameworkStatsLog; +import java.util.Collection; +import libcore.util.NativeAllocationRegistry; /** * Used to wrap different logging calls in one, so that client side code base is clean and more @@ -49,4 +55,46 @@ public class MetricsLoggerWrapper { } } } + + public static void logPostGcMemorySnapshot() { + if (!com.android.libcore.Flags.nativeMetrics()) { + return; + } + int pid = Process.myPid(); + String processName = Application.getProcessName(); + Collection<NativeAllocationRegistry.Metrics> metrics = + NativeAllocationRegistry.getMetrics(); + int nMetrics = metrics.size(); + + String[] classNames = new String[nMetrics]; + long[] mallocedCount = new long[nMetrics]; + long[] mallocedBytes = new long[nMetrics]; + long[] nonmallocedCount = new long[nMetrics]; + long[] nonmallocedBytes = new long[nMetrics]; + + int i = 0; + for (NativeAllocationRegistry.Metrics m : metrics) { + classNames[i] = m.getClassName(); + mallocedCount[i] = m.getMallocedCount(); + mallocedBytes[i] = m.getMallocedBytes(); + nonmallocedCount[i] = m.getNonmallocedCount(); + nonmallocedBytes[i] = m.getNonmallocedBytes(); + i++; + } + + ProcfsMemoryUtil.MemorySnapshot m = ProcfsMemoryUtil.readMemorySnapshotFromProcfs(); + int oom_score_adj = ProcfsMemoryUtil.readOomScoreAdjFromProcfs(); + FrameworkStatsLog.write(FrameworkStatsLog.POSTGC_MEMORY_SNAPSHOT, + m.uid, processName, pid, + oom_score_adj, + m.rssInKilobytes, + m.anonRssInKilobytes, + m.swapInKilobytes, + m.anonRssInKilobytes + m.swapInKilobytes, + classNames, + mallocedCount, + mallocedBytes, + nonmallocedCount, + nonmallocedBytes); + } } diff --git a/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java b/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java new file mode 100644 index 000000000000..1acb34f73002 --- /dev/null +++ b/core/java/com/android/internal/protolog/AutoClosableProtoInputStream.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.protolog; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.proto.ProtoInputStream; + +import java.io.FileInputStream; +import java.io.IOException; + +public final class AutoClosableProtoInputStream implements AutoCloseable { + @NonNull + private final ProtoInputStream mProtoInputStream; + @Nullable + private final FileInputStream mFileInputStream; + + public AutoClosableProtoInputStream(@NonNull FileInputStream fileInputStream) { + mProtoInputStream = new ProtoInputStream(fileInputStream); + mFileInputStream = fileInputStream; + } + + public AutoClosableProtoInputStream(@NonNull byte[] input) { + mProtoInputStream = new ProtoInputStream(input); + mFileInputStream = null; + } + + /** + * @return the ProtoInputStream this class is wrapping + */ + @NonNull + public ProtoInputStream get() { + return mProtoInputStream; + } + + @Override + public void close() throws IOException { + if (mFileInputStream != null) { + mFileInputStream.close(); + } + } +} diff --git a/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java b/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java new file mode 100644 index 000000000000..1598766412dd --- /dev/null +++ b/core/java/com/android/internal/protolog/NoViewerConfigProtoLogImpl.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.protolog; + +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.protolog.common.ILogger; +import com.android.internal.protolog.common.IProtoLog; +import com.android.internal.protolog.common.IProtoLogGroup; +import com.android.internal.protolog.common.LogLevel; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Class should only be used as a temporary solution to missing viewer config file on device. + * In particular this class should only be initialized in Robolectric tests, if it's being used + * otherwise please report it. + * + * @deprecated + */ +@Deprecated +public class NoViewerConfigProtoLogImpl implements IProtoLog { + private static final String LOG_TAG = "ProtoLog"; + + @Override + public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask, + Object[] args) { + Log.w(LOG_TAG, "ProtoLogging is not available due to missing viewer config file..."); + logMessage(logLevel, group.getTag(), "PROTOLOG#" + messageHash + "(" + + Arrays.stream(args).map(Object::toString).collect(Collectors.joining()) + ")"); + } + + @Override + public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args) { + logMessage(logLevel, group.getTag(), TextUtils.formatSimple(messageString, args)); + } + + @Override + public boolean isProtoEnabled() { + return false; + } + + @Override + public int startLoggingToLogcat(String[] groups, ILogger logger) { + return 0; + } + + @Override + public int stopLoggingToLogcat(String[] groups, ILogger logger) { + return 0; + } + + @Override + public boolean isEnabled(IProtoLogGroup group, LogLevel level) { + return false; + } + + @Override + public List<IProtoLogGroup> getRegisteredGroups() { + return List.of(); + } + + private void logMessage(LogLevel logLevel, String tag, String message) { + switch (logLevel) { + case VERBOSE -> Log.v(tag, message); + case INFO -> Log.i(tag, message); + case DEBUG -> Log.d(tag, message); + case WARN -> Log.w(tag, message); + case ERROR -> Log.e(tag, message); + case WTF -> Log.wtf(tag, message); + } + } +} diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index a037cb421b0c..a1c987f79304 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -60,18 +60,16 @@ import android.util.ArraySet; import android.util.Log; import android.util.LongArray; import android.util.Slog; -import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs; import com.android.internal.protolog.common.ILogger; import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogDataType; import com.android.internal.protolog.common.LogLevel; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -93,26 +91,18 @@ import java.util.stream.Stream; /** * A service for the ProtoLog logging system. */ -public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog { +public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProtoLog { private static final String LOG_TAG = "ProtoLog"; public static final String NULL_STRING = "null"; private final AtomicInteger mTracingInstances = new AtomicInteger(); @NonNull - private final ProtoLogDataSource mDataSource; - @Nullable - private final ProtoLogViewerConfigReader mViewerConfigReader; - @Deprecated - @Nullable - private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; + protected final ProtoLogDataSource mDataSource; @NonNull - private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>(); + protected final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>(); @NonNull private final Runnable mCacheUpdater; - @Nullable // null when the flag android.tracing.client_side_proto_logging is not flipped - private final IProtoLogConfigurationService mProtoLogConfigurationService; - @NonNull private final int[] mDefaultLogLevelCounts = new int[LogLevel.values().length]; @NonNull @@ -121,68 +111,15 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto private final Map<String, Integer> mCollectStackTraceGroupCounts = new ArrayMap<>(); private final Lock mBackgroundServiceLock = new ReentrantLock(); - private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); - - public PerfettoProtoLogImpl(@NonNull IProtoLogGroup[] groups) - throws ServiceManager.ServiceNotFoundException { - this(null, null, null, () -> {}, groups); - } - - public PerfettoProtoLogImpl(@NonNull Runnable cacheUpdater, @NonNull IProtoLogGroup[] groups) - throws ServiceManager.ServiceNotFoundException { - this(null, null, null, cacheUpdater, groups); - } - - public PerfettoProtoLogImpl( - @NonNull String viewerConfigFilePath, - @NonNull Runnable cacheUpdater, - @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException { - this(viewerConfigFilePath, - null, - new ProtoLogViewerConfigReader(() -> { - try { - return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); - } catch (FileNotFoundException e) { - throw new RuntimeException( - "Failed to load viewer config file " + viewerConfigFilePath, e); - } - }), - cacheUpdater, groups); - } - - @Deprecated - @VisibleForTesting - public PerfettoProtoLogImpl( - @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, - @Nullable ProtoLogViewerConfigReader viewerConfigReader, - @NonNull Runnable cacheUpdater, - @NonNull IProtoLogGroup[] groups, - @NonNull ProtoLogDataSourceBuilder dataSourceBuilder, - @NonNull ProtoLogConfigurationService configurationService) { - this(null, viewerConfigInputStreamProvider, viewerConfigReader, cacheUpdater, - groups, dataSourceBuilder, configurationService); - } + protected ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); - @VisibleForTesting - public PerfettoProtoLogImpl( - @Nullable String viewerConfigFilePath, - @Nullable ProtoLogViewerConfigReader viewerConfigReader, - @NonNull Runnable cacheUpdater, - @NonNull IProtoLogGroup[] groups, - @NonNull ProtoLogDataSourceBuilder dataSourceBuilder, - @NonNull ProtoLogConfigurationService configurationService) { - this(viewerConfigFilePath, null, viewerConfigReader, cacheUpdater, - groups, dataSourceBuilder, configurationService); - } + // Set to true once this is ready to accept protolog to logcat requests. + private boolean mLogcatReady = false; - private PerfettoProtoLogImpl( - @Nullable String viewerConfigFilePath, - @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, - @Nullable ProtoLogViewerConfigReader viewerConfigReader, + protected PerfettoProtoLogImpl( @NonNull Runnable cacheUpdater, @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException { - this(viewerConfigFilePath, viewerConfigInputStreamProvider, viewerConfigReader, - cacheUpdater, groups, + this(cacheUpdater, groups, ProtoLogDataSource::new, android.tracing.Flags.clientSideProtoLogging() ? IProtoLogConfigurationService.Stub.asInterface( @@ -191,19 +128,11 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto ); } - private PerfettoProtoLogImpl( - @Nullable String viewerConfigFilePath, - @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, - @Nullable ProtoLogViewerConfigReader viewerConfigReader, + protected PerfettoProtoLogImpl( @NonNull Runnable cacheUpdater, @NonNull IProtoLogGroup[] groups, @NonNull ProtoLogDataSourceBuilder dataSourceBuilder, @Nullable IProtoLogConfigurationService configurationService) { - if (viewerConfigFilePath != null && viewerConfigInputStreamProvider != null) { - throw new RuntimeException("Only one of viewerConfigFilePath and " - + "viewerConfigInputStreamProvider can be set"); - } - mDataSource = dataSourceBuilder.build( this::onTracingInstanceStart, this::onTracingFlush, @@ -219,55 +148,33 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto // for some messages logged right after the construction of this class. mDataSource.register(params); - if (viewerConfigInputStreamProvider == null && viewerConfigFilePath != null) { - viewerConfigInputStreamProvider = new ViewerConfigInputStreamProvider() { - @NonNull - @Override - public ProtoInputStream getInputStream() { - try { - return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); - } catch (FileNotFoundException e) { - throw new RuntimeException( - "Failed to load viewer config file " + viewerConfigFilePath, e); - } - } - }; - } - - this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; - this.mViewerConfigReader = viewerConfigReader; this.mCacheUpdater = cacheUpdater; registerGroupsLocally(groups); if (android.tracing.Flags.clientSideProtoLogging()) { - mProtoLogConfigurationService = configurationService; - Objects.requireNonNull(mProtoLogConfigurationService, + Objects.requireNonNull(configurationService, "A null ProtoLog Configuration Service was provided!"); try { - var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs(); - - if (viewerConfigFilePath != null) { - args.setViewerConfigFile(viewerConfigFilePath); - } + var args = createConfigurationServiceRegisterClientArgs(); final var groupArgs = Stream.of(groups) - .map(group -> new ProtoLogConfigurationServiceImpl.RegisterClientArgs + .map(group -> new RegisterClientArgs .GroupConfig(group.name(), group.isLogToLogcat())) - .toArray(ProtoLogConfigurationServiceImpl - .RegisterClientArgs.GroupConfig[]::new); + .toArray(RegisterClientArgs.GroupConfig[]::new); args.setGroups(groupArgs); - mProtoLogConfigurationService.registerClient(this, args); + configurationService.registerClient(this, args); } catch (RemoteException e) { throw new RuntimeException("Failed to register ProtoLog client"); } - } else { - mProtoLogConfigurationService = null; } } + @NonNull + protected abstract RegisterClientArgs createConfigurationServiceRegisterClientArgs(); + /** * Main log method, do not call directly. */ @@ -334,9 +241,6 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto * @return status code */ public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) { - if (mViewerConfigReader != null) { - mViewerConfigReader.loadViewerConfig(groups, logger); - } return setTextLogging(true, logger, groups); } @@ -347,9 +251,6 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto * @return status code */ public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) { - if (mViewerConfigReader != null) { - mViewerConfigReader.unloadViewerConfig(groups, logger); - } return setTextLogging(false, logger, groups); } @@ -372,21 +273,8 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto // we might want to manually specify an id for the group with a collision verifyNoCollisionsOrDuplicates(protoLogGroups); - final var groupsLoggingToLogcat = new ArrayList<String>(); for (IProtoLogGroup protoLogGroup : protoLogGroups) { mLogGroups.put(protoLogGroup.name(), protoLogGroup); - - if (protoLogGroup.isLogToLogcat()) { - groupsLoggingToLogcat.add(protoLogGroup.name()); - } - } - - if (mViewerConfigReader != null) { - // Load in background to avoid delay in boot process. - // The caveat is that any log message that is also logged to logcat will not be - // successfully decoded until this completes. - mBackgroundLoggingService.execute(() -> mViewerConfigReader - .loadViewerConfig(groupsLoggingToLogcat.toArray(new String[0]))); } } @@ -403,6 +291,10 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto } } + protected void readyToLogToLogcat() { + mLogcatReady = true; + } + /** * Responds to a shell command. */ @@ -499,57 +391,21 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto } @Deprecated - private void dumpViewerConfig() { - if (mViewerConfigInputStreamProvider == null) { - // No viewer config available - return; - } - - Log.d(LOG_TAG, "Dumping viewer config to trace"); + abstract void dumpViewerConfig(); - Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider); - - Log.d(LOG_TAG, "Dumped viewer config to trace"); - } + @NonNull + abstract String getLogcatMessageString(@NonNull Message message); - private void logToLogcat(String tag, LogLevel level, Message message, + private void logToLogcat(@NonNull String tag, @NonNull LogLevel level, @NonNull Message message, @Nullable Object[] args) { - String messageString; - if (mViewerConfigReader == null) { - messageString = message.getMessage(); - - if (messageString == null) { - Log.e(LOG_TAG, "Failed to decode message for logcat. " - + "Message not available without ViewerConfig to decode the hash."); - } - } else { - messageString = message.getMessage(mViewerConfigReader); - - if (messageString == null) { - Log.e(LOG_TAG, "Failed to decode message for logcat. " - + "Message hash either not available in viewerConfig file or " - + "not loaded into memory from file before decoding."); - } - } - - if (messageString == null) { - StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE"); - if (args != null) { - builder.append(" args = ("); - builder.append(String.join(", ", Arrays.stream(args) - .map(it -> { - if (it == null) { - return "null"; - } else { - return it.toString(); - } - }).toList())); - builder.append(")"); - } - messageString = builder.toString(); - args = new Object[0]; + if (!mLogcatReady) { + Log.w(LOG_TAG, "Trying to log a protolog message with hash " + + message.getMessageHash() + " to logcat before the service is ready to accept " + + "such requests."); + return; } + String messageString = getLogcatMessageString(message); logToLogcat(tag, level, messageString, args); } diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java new file mode 100644 index 000000000000..febe1f3a72ac --- /dev/null +++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.protolog; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.ServiceManager; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs; +import com.android.internal.protolog.common.ILogger; +import com.android.internal.protolog.common.IProtoLogGroup; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; + +public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl { + private static final String LOG_TAG = "PerfettoProtoLogImpl"; + + @NonNull + private final ProtoLogViewerConfigReader mViewerConfigReader; + @Deprecated + @NonNull + private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; + @NonNull + private final String mViewerConfigFilePath; + + public ProcessedPerfettoProtoLogImpl( + @NonNull String viewerConfigFilePath, + @NonNull Runnable cacheUpdater, + @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException { + this(viewerConfigFilePath, new ViewerConfigInputStreamProvider() { + @NonNull + @Override + public AutoClosableProtoInputStream getInputStream() { + try { + final var protoFileInputStream = + new FileInputStream(viewerConfigFilePath); + return new AutoClosableProtoInputStream(protoFileInputStream); + } catch (FileNotFoundException e) { + throw new RuntimeException( + "Failed to load viewer config file " + viewerConfigFilePath, e); + } + } + }, + cacheUpdater, groups); + } + + @VisibleForTesting + public ProcessedPerfettoProtoLogImpl( + @NonNull String viewerConfigFilePath, + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, + @NonNull Runnable cacheUpdater, + @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException { + super(cacheUpdater, groups); + + this.mViewerConfigFilePath = viewerConfigFilePath; + + this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; + this.mViewerConfigReader = new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider); + + loadLogcatGroupsViewerConfig(groups); + } + + @VisibleForTesting + public ProcessedPerfettoProtoLogImpl( + @NonNull String viewerConfigFilePath, + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, + @NonNull ProtoLogViewerConfigReader viewerConfigReader, + @NonNull Runnable cacheUpdater, + @NonNull IProtoLogGroup[] groups, + @NonNull ProtoLogDataSourceBuilder dataSourceBuilder, + @Nullable IProtoLogConfigurationService configurationService) + throws ServiceManager.ServiceNotFoundException { + super(cacheUpdater, groups, dataSourceBuilder, configurationService); + + this.mViewerConfigFilePath = viewerConfigFilePath; + + this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; + this.mViewerConfigReader = viewerConfigReader; + + loadLogcatGroupsViewerConfig(groups); + } + + @NonNull + @Override + protected RegisterClientArgs createConfigurationServiceRegisterClientArgs() { + return new RegisterClientArgs() + .setViewerConfigFile(mViewerConfigFilePath); + } + + /** + * Start text logging + * @param groups Groups to start text logging for + * @param logger A logger to write status updates to + * @return status code + */ + @Override + public int startLoggingToLogcat(String[] groups, @NonNull ILogger logger) { + mViewerConfigReader.loadViewerConfig(groups, logger); + return super.startLoggingToLogcat(groups, logger); + } + + /** + * Stop text logging + * @param groups Groups to start text logging for + * @param logger A logger to write status updates to + * @return status code + */ + @Override + public int stopLoggingToLogcat(String[] groups, @NonNull ILogger logger) { + mViewerConfigReader.unloadViewerConfig(groups, logger); + return super.stopLoggingToLogcat(groups, logger); + } + + @Deprecated + @Override + void dumpViewerConfig() { + Log.d(LOG_TAG, "Dumping viewer config to trace"); + Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider); + Log.d(LOG_TAG, "Dumped viewer config to trace"); + } + + @NonNull + @Override + String getLogcatMessageString(@NonNull Message message) { + String messageString; + messageString = message.getMessage(mViewerConfigReader); + + if (messageString == null) { + throw new RuntimeException("Failed to decode message for logcat. " + + "Message hash (" + message.getMessageHash() + ") either not available in " + + "viewerConfig file (" + mViewerConfigFilePath + ") or " + + "not loaded into memory from file before decoding."); + } + + return messageString; + } + + private void loadLogcatGroupsViewerConfig(@NonNull IProtoLogGroup[] protoLogGroups) { + final var groupsLoggingToLogcat = new ArrayList<String>(); + for (IProtoLogGroup protoLogGroup : protoLogGroups) { + if (protoLogGroup.isLogToLogcat()) { + groupsLoggingToLogcat.add(protoLogGroup.name()); + } + } + + // Load in background to avoid delay in boot process. + // The caveat is that any log message that is also logged to logcat will not be + // successfully decoded until this completes. + mBackgroundLoggingService.execute(() -> { + mViewerConfigReader.loadViewerConfig(groupsLoggingToLogcat.toArray(new String[0])); + readyToLogToLogcat(); + }); + } +} diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java index 60213b1830c6..d117e93d7de7 100644 --- a/core/java/com/android/internal/protolog/ProtoLog.java +++ b/core/java/com/android/internal/protolog/ProtoLog.java @@ -70,16 +70,16 @@ public class ProtoLog { // directly to the generated tracing implementations. if (android.tracing.Flags.perfettoProtologTracing()) { synchronized (sInitLock) { + final var allGroups = new HashSet<>(Arrays.stream(groups).toList()); if (sProtoLogInstance != null) { // The ProtoLog instance has already been initialized in this process final var alreadyRegisteredGroups = sProtoLogInstance.getRegisteredGroups(); - final var allGroups = new HashSet<>(alreadyRegisteredGroups); - allGroups.addAll(Arrays.stream(groups).toList()); - groups = allGroups.toArray(new IProtoLogGroup[0]); + allGroups.addAll(alreadyRegisteredGroups); } try { - sProtoLogInstance = new PerfettoProtoLogImpl(groups); + sProtoLogInstance = new UnprocessedPerfettoProtoLogImpl( + allGroups.toArray(new IProtoLogGroup[0])); } catch (ServiceManager.ServiceNotFoundException e) { throw new RuntimeException(e); } diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java index 8d37899f8602..e9a8770deb73 100644 --- a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java @@ -379,7 +379,7 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ @NonNull String viewerConfigFilePath) { Utils.dumpViewerConfig(dataSource, () -> { try { - return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); + return new AutoClosableProtoInputStream(new FileInputStream(viewerConfigFilePath)); } catch (FileNotFoundException e) { throw new RuntimeException( "Failed to load viewer config file " + viewerConfigFilePath, e); diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 5d67534b1b44..3378d08e7761 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -105,31 +105,10 @@ public class ProtoLogImpl { + "viewerConfigPath = " + sViewerConfigPath); final var groups = sLogGroups.values().toArray(new IProtoLogGroup[0]); - if (android.tracing.Flags.perfettoProtologTracing()) { - try { - File f = new File(sViewerConfigPath); - if (!ProtoLog.REQUIRE_PROTOLOGTOOL && !f.exists()) { - // TODO(b/353530422): Remove - temporary fix to unblock b/352290057 - // In some tests the viewer config file might not exist in which we don't - // want to provide config path to the user - Log.w(LOG_TAG, "Failed to find viewerConfigFile when setting up " - + ProtoLogImpl.class.getSimpleName() + ". " - + "Setting up without a viewer config instead..."); - - sServiceInstance = new PerfettoProtoLogImpl(sCacheUpdater, groups); - } else { - sServiceInstance = - new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater, groups); - } - } catch (ServiceManager.ServiceNotFoundException e) { - throw new RuntimeException(e); - } + sServiceInstance = createProtoLogImpl(groups); } else { - var protologImpl = new LegacyProtoLogImpl( - sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater); - protologImpl.registerGroups(groups); - sServiceInstance = protologImpl; + sServiceInstance = createLegacyProtoLogImpl(groups); } sCacheUpdater.run(); @@ -137,6 +116,34 @@ public class ProtoLogImpl { return sServiceInstance; } + private static IProtoLog createProtoLogImpl(IProtoLogGroup[] groups) { + try { + File f = new File(sViewerConfigPath); + if (!f.exists()) { + // TODO(b/353530422): Remove - temporary fix to unblock b/352290057 + // In robolectric tests the viewer config file isn't current available, so we cannot + // use the ProcessedPerfettoProtoLogImpl. + Log.e(LOG_TAG, "Failed to find viewer config file " + sViewerConfigPath + + " when setting up " + ProtoLogImpl.class.getSimpleName() + ". " + + "ProtoLog will not work here!"); + + return new NoViewerConfigProtoLogImpl(); + } else { + return new ProcessedPerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater, groups); + } + } catch (ServiceManager.ServiceNotFoundException e) { + throw new RuntimeException(e); + } + } + + private static LegacyProtoLogImpl createLegacyProtoLogImpl(IProtoLogGroup[] groups) { + var protologImpl = new LegacyProtoLogImpl( + sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater); + protologImpl.registerGroups(groups); + + return protologImpl; + } + @VisibleForTesting public static synchronized void setSingleInstance(@Nullable IProtoLog instance) { sServiceInstance = instance; diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java index 571fe0ba37b2..524f64225084 100644 --- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java +++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java @@ -106,46 +106,47 @@ public class ProtoLogViewerConfigReader { long targetGroupId = loadGroupId(group); final Map<Long, String> hashesForGroup = new TreeMap<>(); - final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream(); - - while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - if (pis.getFieldNumber() == (int) MESSAGES) { - final long inMessageToken = pis.start(MESSAGES); - - long messageId = 0; - String message = null; - int groupId = 0; - while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - switch (pis.getFieldNumber()) { - case (int) MESSAGE_ID: - messageId = pis.readLong(MESSAGE_ID); - break; - case (int) MESSAGE: - message = pis.readString(MESSAGE); - break; - case (int) GROUP_ID: - groupId = pis.readInt(GROUP_ID); - break; + try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) { + final var pis = pisWrapper.get(); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (pis.getFieldNumber() == (int) MESSAGES) { + final long inMessageToken = pis.start(MESSAGES); + + long messageId = 0; + String message = null; + int groupId = 0; + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) MESSAGE_ID: + messageId = pis.readLong(MESSAGE_ID); + break; + case (int) MESSAGE: + message = pis.readString(MESSAGE); + break; + case (int) GROUP_ID: + groupId = pis.readInt(GROUP_ID); + break; + } } - } - if (groupId == 0) { - throw new IOException("Failed to get group id"); - } + if (groupId == 0) { + throw new IOException("Failed to get group id"); + } - if (messageId == 0) { - throw new IOException("Failed to get message id"); - } + if (messageId == 0) { + throw new IOException("Failed to get message id"); + } - if (message == null) { - throw new IOException("Failed to get message string"); - } + if (message == null) { + throw new IOException("Failed to get message string"); + } - if (groupId == targetGroupId) { - hashesForGroup.put(messageId, message); - } + if (groupId == targetGroupId) { + hashesForGroup.put(messageId, message); + } - pis.end(inMessageToken); + pis.end(inMessageToken); + } } } @@ -153,30 +154,32 @@ public class ProtoLogViewerConfigReader { } private long loadGroupId(@NonNull String group) throws IOException { - final ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream(); - - while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - if (pis.getFieldNumber() == (int) GROUPS) { - final long inMessageToken = pis.start(GROUPS); - - long groupId = 0; - String groupName = null; - while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - switch (pis.getFieldNumber()) { - case (int) ID: - groupId = pis.readInt(ID); - break; - case (int) NAME: - groupName = pis.readString(NAME); - break; + try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) { + final var pis = pisWrapper.get(); + + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (pis.getFieldNumber() == (int) GROUPS) { + final long inMessageToken = pis.start(GROUPS); + + long groupId = 0; + String groupName = null; + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) ID: + groupId = pis.readInt(ID); + break; + case (int) NAME: + groupName = pis.readString(NAME); + break; + } } - } - if (Objects.equals(groupName, group)) { - return groupId; - } + if (Objects.equals(groupName, group)) { + return groupId; + } - pis.end(inMessageToken); + pis.end(inMessageToken); + } } } diff --git a/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java new file mode 100644 index 000000000000..f3fe58070fa9 --- /dev/null +++ b/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.protolog; + +import android.annotation.NonNull; +import android.os.ServiceManager; + +import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs; +import com.android.internal.protolog.common.IProtoLogGroup; + +public class UnprocessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl { + public UnprocessedPerfettoProtoLogImpl(@NonNull IProtoLogGroup[] groups) + throws ServiceManager.ServiceNotFoundException { + this(() -> {}, groups); + } + + public UnprocessedPerfettoProtoLogImpl(@NonNull Runnable cacheUpdater, + @NonNull IProtoLogGroup[] groups) throws ServiceManager.ServiceNotFoundException { + super(cacheUpdater, groups); + readyToLogToLogcat(); + } + + @NonNull + @Override + protected RegisterClientArgs createConfigurationServiceRegisterClientArgs() { + return new RegisterClientArgs(); + } + + @Override + void dumpViewerConfig() { + // No-op + } + + @NonNull + @Override + String getLogcatMessageString(@NonNull Message message) { + String messageString; + messageString = message.getMessage(); + + if (messageString == null) { + throw new RuntimeException("Failed to decode message for logcat. " + + "Message not available without ViewerConfig to decode the hash."); + } + + return messageString; + } +} diff --git a/core/java/com/android/internal/protolog/Utils.java b/core/java/com/android/internal/protolog/Utils.java index 00ef80ab2bdd..629682ca2e71 100644 --- a/core/java/com/android/internal/protolog/Utils.java +++ b/core/java/com/android/internal/protolog/Utils.java @@ -48,8 +48,8 @@ public class Utils { public static void dumpViewerConfig(@NonNull ProtoLogDataSource dataSource, @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { dataSource.trace(ctx -> { - try { - ProtoInputStream pis = viewerConfigInputStreamProvider.getInputStream(); + try (var pisWrapper = viewerConfigInputStreamProvider.getInputStream()) { + final var pis = pisWrapper.get(); final ProtoOutputStream os = ctx.newTracePacket(); diff --git a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java index 14bc8e4782f2..60c98923eb23 100644 --- a/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java +++ b/core/java/com/android/internal/protolog/ViewerConfigInputStreamProvider.java @@ -17,12 +17,12 @@ package com.android.internal.protolog; import android.annotation.NonNull; -import android.util.proto.ProtoInputStream; public interface ViewerConfigInputStreamProvider { /** * @return a ProtoInputStream. */ @NonNull - ProtoInputStream getInputStream(); + AutoClosableProtoInputStream getInputStream(); } + diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl index 81b885aa626b..b5c87868af12 100644 --- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl +++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl @@ -84,4 +84,5 @@ oneway interface IPhoneStateListener { void onSimultaneousCallingStateChanged(in int[] subIds); void onCarrierRoamingNtnModeChanged(in boolean active); void onCarrierRoamingNtnEligibleStateChanged(in boolean eligible); + void onCarrierRoamingNtnAvailableServicesChanged(in int[] availableServices); } diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl index f836cf2b9d87..ca75abdedfcc 100644 --- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -123,4 +123,5 @@ interface ITelephonyRegistry { void notifyCallbackModeStopped(int phoneId, int subId, int type, int reason); void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active); void notifyCarrierRoamingNtnEligibleStateChanged(int subId, in boolean eligible); + void notifyCarrierRoamingNtnAvailableServicesChanged(int subId, in int[] availableServices); } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 42ac90dd8066..9c92e5ca589b 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7178,4 +7178,7 @@ <!-- The name of the service for forensic backup transport. --> <string name="config_forensicBackupTransport" translatable="false"></string> + + <!-- Whether to enable fp unlock when screen turns off on udfps devices --> + <bool name="config_screen_off_udfps_enabled">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index dfee85a3788d..712b99439496 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5639,4 +5639,7 @@ <!-- Forensic backup transport --> <java-symbol type="string" name="config_forensicBackupTransport" /> + + <!-- Fingerprint screen off unlock config --> + <java-symbol type="bool" name="config_screen_off_udfps_enabled" /> </resources> diff --git a/core/tests/vibrator/src/android/os/VibratorInfoTest.java b/core/tests/vibrator/src/android/os/VibratorInfoTest.java index 04945f38e319..9099918edb02 100644 --- a/core/tests/vibrator/src/android/os/VibratorInfoTest.java +++ b/core/tests/vibrator/src/android/os/VibratorInfoTest.java @@ -71,8 +71,7 @@ public class VibratorInfoTest { VibratorInfo noCapabilities = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); assertFalse(noCapabilities.hasFrequencyControl()); VibratorInfo composeAndFrequencyControl = new VibratorInfo.Builder(TEST_VIBRATOR_ID) - .setCapabilities( - IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS) + .setCapabilities(IVibrator.CAP_FREQUENCY_CONTROL) .build(); assertTrue(composeAndFrequencyControl.hasFrequencyControl()); } @@ -153,7 +152,8 @@ public class VibratorInfoTest { VibratorInfo noCapabilities = new VibratorInfo.Builder(TEST_VIBRATOR_ID).build(); assertFalse(noCapabilities.areEnvelopeEffectsSupported()); VibratorInfo envelopeEffectCapability = new VibratorInfo.Builder(TEST_VIBRATOR_ID) - .setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2) + .setCapabilities( + IVibrator.CAP_FREQUENCY_CONTROL | IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2) .build(); assertTrue(envelopeEffectCapability.areEnvelopeEffectsSupported()); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java index 6ad2f088ce95..220fc6f82a71 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java @@ -54,6 +54,7 @@ class BackupHelper { @NonNull private final BackupIdler mBackupIdler = new BackupIdler(); private boolean mBackupIdlerScheduled; + private boolean mSaveEmbeddingState = false; private final List<ParcelableTaskContainerData> mParcelableTaskContainerDataList = new ArrayList<>(); @@ -71,11 +72,32 @@ class BackupHelper { } } + void setAutoSaveEmbeddingState(boolean saveEmbeddingState) { + if (mSaveEmbeddingState == saveEmbeddingState) { + return; + } + + Log.i(TAG, "Set save embedding state: " + saveEmbeddingState); + mSaveEmbeddingState = saveEmbeddingState; + if (!mSaveEmbeddingState) { + removeSavedState(); + return; + } + + if (!hasPendingStateToRestore() && !mController.getTaskContainers().isEmpty()) { + scheduleBackup(); + } + } /** * Schedules a back-up request. It is no-op if there was a request scheduled and not yet * completed. */ void scheduleBackup() { + if (!mSaveEmbeddingState) { + // TODO(b/289875940): enabled internally for broader testing. + return; + } + if (!mBackupIdlerScheduled) { mBackupIdlerScheduled = true; Looper.getMainLooper().getQueue().addIdleHandler(mBackupIdler); @@ -128,7 +150,6 @@ class BackupHelper { final List<TaskFragmentInfo> infos = savedState.getParcelableArrayList( KEY_RESTORE_TASK_FRAGMENTS_INFO, TaskFragmentInfo.class); for (TaskFragmentInfo info : infos) { - if (DEBUG) Log.d(TAG, "Retrieved: " + info); mTaskFragmentInfos.put(info.getFragmentToken(), info); mPresenter.updateTaskFragmentInfo(info); } @@ -140,6 +161,11 @@ class BackupHelper { if (DEBUG) Log.d(TAG, "Retrieved: " + info); mTaskFragmentParentInfos.put(info.getTaskId(), info); } + + if (DEBUG) { + Log.d(TAG, "Retrieved task-fragment info: " + infos.size() + ", task info: " + + parentInfos.size()); + } } void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) { @@ -148,7 +174,6 @@ class BackupHelper { final TaskFragmentInfo info = mTaskFragmentInfos.valueAt(i); mPresenter.deleteTaskFragment(wct, info.getFragmentToken()); } - removeSavedState(); } @@ -190,6 +215,9 @@ class BackupHelper { final ArrayMap<String, EmbeddingRule> embeddingRuleMap = new ArrayMap<>(); for (EmbeddingRule rule : rules) { embeddingRuleMap.put(rule.getTag(), rule); + if (DEBUG) { + Log.d(TAG, "Tag: " + rule.getTag() + " rule: " + rule); + } } boolean restoredAny = false; @@ -201,7 +229,7 @@ class BackupHelper { // has unknown tag, unable to restore. if (DEBUG) { Log.d(TAG, "Rebuilding TaskContainer abort! Unknown Tag. Task#" - + parcelableTaskContainerData.mTaskId); + + parcelableTaskContainerData.mTaskId + ", tags = " + tags); } continue; } @@ -217,7 +245,7 @@ class BackupHelper { final TaskContainer taskContainer = new TaskContainer(parcelableTaskContainerData, mController, mTaskFragmentInfos); - if (DEBUG) Log.d(TAG, "Created TaskContainer " + taskContainer); + if (DEBUG) Log.d(TAG, "Created TaskContainer " + taskContainer.getTaskId()); mController.addTaskContainer(taskContainer.getTaskId(), taskContainer); for (ParcelableSplitContainerData splitData : diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 3368e2eab3ad..60e1a506ab73 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -2886,6 +2886,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return getActiveSplitForContainer(container) != null; } + + @Override + public void setAutoSaveEmbeddingState(boolean saveEmbeddingState) { + if (!Flags.aeBackStackRestore()) { + return; + } + + synchronized (mLock) { + mPresenter.setAutoSaveEmbeddingState(saveEmbeddingState); + } + } + void scheduleBackup() { synchronized (mLock) { mPresenter.scheduleBackup(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index b498ee2ff438..9a2f32e9ee99 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -183,6 +183,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } } + void setAutoSaveEmbeddingState(boolean saveEmbeddingState) { + mBackupHelper.setAutoSaveEmbeddingState(saveEmbeddingState); + } + void scheduleBackup() { mBackupHelper.scheduleBackup(); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index b453f1d4e936..6928409fd819 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -48,8 +48,6 @@ import androidx.window.extensions.embedding.SplitAttributes.SplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; -import com.android.window.flags.Flags; - import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -634,11 +632,7 @@ class TaskContainer { // pin container. updateAlwaysOnTopOverlayIfNecessary(); - // TODO(b/289875940): Making backup-restore as an opt-in solution, before the flag goes - // to next-food. - if (Flags.aeBackStackRestore()) { - mSplitController.scheduleBackup(); - } + mSplitController.scheduleBackup(); } private void updateAlwaysOnTopOverlayIfNecessary() { diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 1260796810c2..b2ac640a468d 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -25,6 +25,7 @@ <uses-permission android:name="android.permission.READ_FRAME_BUFFER" /> <uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" /> <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" /> + <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" /> <application> <activity 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 03a851bb9507..4c2588984500 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 @@ -24,6 +24,7 @@ import android.annotation.Nullable; import android.app.KeyguardManager; import android.content.Context; import android.content.pm.LauncherApps; +import android.hardware.input.InputManager; import android.os.Handler; import android.os.UserManager; import android.view.Choreographer; @@ -266,7 +267,8 @@ public abstract class WMShellModule { AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler, - FocusTransitionObserver focusTransitionObserver) { + FocusTransitionObserver focusTransitionObserver, + DesktopModeEventLogger desktopModeEventLogger) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return new DesktopModeWindowDecorViewModel( context, @@ -294,7 +296,8 @@ public abstract class WMShellModule { appHandleEducationController, windowDecorCaptionHandleRepository, desktopActivityOrientationHandler, - focusTransitionObserver); + focusTransitionObserver, + desktopModeEventLogger); } return new CaptionWindowDecorViewModel( context, @@ -644,7 +647,10 @@ public abstract class WMShellModule { @ShellMainThread Handler mainHandler, Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<RecentTasksController> recentTasksController, - InteractionJankMonitor interactionJankMonitor) { + InteractionJankMonitor interactionJankMonitor, + InputManager inputManager, + FocusTransitionObserver focusTransitionObserver, + DesktopModeEventLogger desktopModeEventLogger) { return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, keyguardManager, @@ -655,7 +661,9 @@ public abstract class WMShellModule { desktopRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter, - recentTasksController.orElse(null), interactionJankMonitor, mainHandler); + recentTasksController.orElse(null), interactionJankMonitor, mainHandler, + inputManager, focusTransitionObserver, + desktopModeEventLogger); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index 8ebe503a3816..255ca6e2511f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -16,11 +16,20 @@ package com.android.wm.shell.desktopmode +import android.app.ActivityManager.RunningTaskInfo +import android.util.Size +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_TOUCHSCREEN +import android.view.MotionEvent +import android.view.MotionEvent.TOOL_TYPE_FINGER +import android.view.MotionEvent.TOOL_TYPE_MOUSE +import android.view.MotionEvent.TOOL_TYPE_STYLUS import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.internal.util.FrameworkStatsLog import com.android.window.flags.Flags import com.android.wm.shell.EventLogTags +import com.android.wm.shell.common.DisplayController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import java.security.SecureRandom import java.util.Random @@ -176,7 +185,13 @@ class DesktopModeEventLogger { * Logs that a task resize event is starting with [taskSizeUpdate] within a Desktop mode * session. */ - fun logTaskResizingStarted(taskSizeUpdate: TaskSizeUpdate) { + fun logTaskResizingStarted( + resizeTrigger: ResizeTrigger, + motionEvent: MotionEvent?, + taskInfo: RunningTaskInfo, + displayController: DisplayController? = null, + displayLayoutSize: Size? = null, + ) { if (!Flags.enableResizingMetrics()) return val sessionId = currentSessionId.get() @@ -188,11 +203,19 @@ class DesktopModeEventLogger { return } + val taskSizeUpdate = createTaskSizeUpdate( + resizeTrigger, + motionEvent, + taskInfo, + displayController = displayController, + displayLayoutSize = displayLayoutSize, + ) + ProtoLog.v( WM_SHELL_DESKTOP_MODE, - "DesktopModeLogger: Logging task resize is starting, session: %s taskId: %s", + "DesktopModeLogger: Logging task resize is starting, session: %s, taskSizeUpdate: %s", sessionId, - taskSizeUpdate.instanceId + taskSizeUpdate ) logTaskSizeUpdated( FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE, @@ -203,7 +226,15 @@ class DesktopModeEventLogger { /** * Logs that a task resize event is ending with [taskSizeUpdate] within a Desktop mode session. */ - fun logTaskResizingEnded(taskSizeUpdate: TaskSizeUpdate) { + fun logTaskResizingEnded( + resizeTrigger: ResizeTrigger, + motionEvent: MotionEvent?, + taskInfo: RunningTaskInfo, + taskHeight: Int? = null, + taskWidth: Int? = null, + displayController: DisplayController? = null, + displayLayoutSize: Size? = null, + ) { if (!Flags.enableResizingMetrics()) return val sessionId = currentSessionId.get() @@ -215,18 +246,61 @@ class DesktopModeEventLogger { return } + val taskSizeUpdate = createTaskSizeUpdate( + resizeTrigger, + motionEvent, + taskInfo, + taskHeight, + taskWidth, + displayController, + displayLayoutSize, + ) + ProtoLog.v( WM_SHELL_DESKTOP_MODE, - "DesktopModeLogger: Logging task resize is ending, session: %s taskId: %s", + "DesktopModeLogger: Logging task resize is ending, session: %s, taskSizeUpdate: %s", sessionId, - taskSizeUpdate.instanceId + taskSizeUpdate ) + logTaskSizeUpdated( FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE, sessionId, taskSizeUpdate ) } + private fun createTaskSizeUpdate( + resizeTrigger: ResizeTrigger, + motionEvent: MotionEvent?, + taskInfo: RunningTaskInfo, + taskHeight: Int? = null, + taskWidth: Int? = null, + displayController: DisplayController? = null, + displayLayoutSize: Size? = null, + ): TaskSizeUpdate { + val taskBounds = taskInfo.configuration.windowConfiguration.bounds + + val height = taskHeight ?: taskBounds.height() + val width = taskWidth ?: taskBounds.width() + + val displaySize = when { + displayLayoutSize != null -> displayLayoutSize.height * displayLayoutSize.width + displayController != null -> displayController.getDisplayLayout(taskInfo.displayId) + ?.let { it.height() * it.width() } + else -> null + } + + return TaskSizeUpdate( + resizeTrigger, + getInputMethodFromMotionEvent(motionEvent), + taskInfo.taskId, + taskInfo.effectiveUid, + height, + width, + displaySize, + ) + } + fun logTaskInfoStateInit() { logTaskUpdate( FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD, @@ -238,7 +312,8 @@ class DesktopModeEventLogger { taskHeight = 0, taskWidth = 0, taskX = 0, - taskY = 0) + taskY = 0 + ) ) } @@ -314,7 +389,7 @@ class DesktopModeEventLogger { /* task_width */ taskSizeUpdate.taskWidth, /* display_area */ - taskSizeUpdate.displayArea + taskSizeUpdate.displayArea ?: -1 ) } @@ -364,9 +439,24 @@ class DesktopModeEventLogger { val uid: Int, val taskHeight: Int, val taskWidth: Int, - val displayArea: Int, + val displayArea: Int?, ) + private fun getInputMethodFromMotionEvent(e: MotionEvent?): InputMethod { + if (e == null) return InputMethod.UNKNOWN_INPUT_METHOD + + val toolType = e.getToolType( + e.findPointerIndex(e.getPointerId(0)) + ) + return when { + toolType == TOOL_TYPE_STYLUS -> InputMethod.STYLUS + toolType == TOOL_TYPE_MOUSE -> InputMethod.MOUSE + toolType == TOOL_TYPE_FINGER && e.source == SOURCE_MOUSE -> InputMethod.TOUCHPAD + toolType == TOOL_TYPE_FINGER && e.source == SOURCE_TOUCHSCREEN -> InputMethod.TOUCH + else -> InputMethod.UNKNOWN_INPUT_METHOD + } + } + // Default value used when the task was not minimized. @VisibleForTesting const val UNSET_MINIMIZE_REASON = @@ -499,6 +589,10 @@ class DesktopModeEventLogger { FrameworkStatsLog .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__SNAP_RIGHT_MENU_RESIZE_TRIGGER ), + MAXIMIZE_MENU( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__MAXIMIZE_MENU_RESIZE_TRIGGER + ), } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index 443e4179b524..85a3126d9de6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -30,7 +30,6 @@ import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository -import com.android.wm.shell.desktopmode.persistence.DesktopTask import com.android.wm.shell.desktopmode.persistence.DesktopTaskState import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread @@ -124,7 +123,8 @@ class DesktopRepository ( if (!Flags.enableDesktopWindowingPersistence()) return // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized mainCoroutineScope.launch { - val desktop = persistentRepository.readDesktop() + val desktop = persistentRepository.readDesktop() ?: return@launch + val maxTasks = DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } ?: desktop.zOrderedTasksCount @@ -132,13 +132,11 @@ class DesktopRepository ( desktop.zOrderedTasksList // Reverse it so we initialize the repo from bottom to top. .reversed() - .map { taskId -> - desktop.tasksByTaskIdMap.getOrDefault( - taskId, - DesktopTask.getDefaultInstance() - ) + .mapNotNull { taskId -> + desktop.tasksByTaskIdMap[taskId]?.takeIf { + it.desktopTaskState == DesktopTaskState.VISIBLE + } } - .filter { task -> task.desktopTaskState == DesktopTaskState.VISIBLE } .take(maxTasks) .forEach { task -> addOrMoveFreeformTaskToTop(desktop.displayId, task.taskId) 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 8dd7589c5937..69776cd4740a 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 @@ -35,6 +35,9 @@ import android.graphics.Point import android.graphics.PointF import android.graphics.Rect import android.graphics.Region +import android.hardware.input.InputManager +import android.hardware.input.InputManager.KeyGestureEventHandler +import android.hardware.input.KeyGestureEvent import android.os.Binder import android.os.Handler import android.os.IBinder @@ -42,6 +45,8 @@ import android.os.SystemProperties import android.util.Size import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent +import android.view.KeyEvent +import android.view.MotionEvent import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE @@ -57,6 +62,7 @@ import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.annotation.BinderThread +import com.android.hardware.input.Flags.useKeyGestureEventHandler import com.android.internal.annotations.VisibleForTesting import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE @@ -65,6 +71,7 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags +import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController @@ -78,12 +85,13 @@ import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing -import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType +import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler import com.android.wm.shell.draganddrop.DragAndDropController +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler @@ -92,7 +100,6 @@ import com.android.wm.shell.shared.ShellSharedConstants import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellMainThread -import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity @@ -105,6 +112,7 @@ import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.UserChangeListener +import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility @@ -118,7 +126,7 @@ import java.io.PrintWriter import java.util.Optional import java.util.concurrent.Executor import java.util.function.Consumer - +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger /** Handles moving tasks in and out of desktop */ class DesktopTasksController( private val context: Context, @@ -149,11 +157,15 @@ class DesktopTasksController( private val recentTasksController: RecentTasksController?, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, + private val inputManager: InputManager, + private val focusTransitionObserver: FocusTransitionObserver, + private val desktopModeEventLogger: DesktopModeEventLogger, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, DragAndDropController.DragAndDropListener, - UserChangeListener { + UserChangeListener, + KeyGestureEventHandler { private val desktopMode: DesktopModeImpl private var visualIndicator: DesktopModeVisualIndicator? = null @@ -226,6 +238,9 @@ class DesktopTasksController( } ) dragAndDropController.addListener(this) + if (useKeyGestureEventHandler() && enableMoveToNextDisplayShortcut()) { + inputManager.registerKeyGestureEventHandler(this) + } } @VisibleForTesting @@ -734,7 +749,11 @@ class DesktopTasksController( * bounds) and a free floating state (either the last saved bounds if available or the default * bounds otherwise). */ - fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) { + fun toggleDesktopTaskSize( + taskInfo: RunningTaskInfo, + resizeTrigger: ResizeTrigger, + motionEvent: MotionEvent?, + ) { val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } @@ -781,7 +800,10 @@ class DesktopTasksController( taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding) val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) - + desktopModeEventLogger.logTaskResizingEnded( + resizeTrigger, motionEvent, taskInfo, destinationBounds.height(), + destinationBounds.width(), displayController + ) toggleResizeDesktopTaskTransitionHandler.startTransition(wct) } @@ -871,9 +893,19 @@ class DesktopTasksController( taskInfo: RunningTaskInfo, taskSurface: SurfaceControl, currentDragBounds: Rect, - position: SnapPosition + position: SnapPosition, + resizeTrigger: ResizeTrigger, + motionEvent: MotionEvent?, ) { val destinationBounds = getSnapBounds(taskInfo, position) + desktopModeEventLogger.logTaskResizingEnded( + resizeTrigger, + motionEvent, + taskInfo, + destinationBounds.height(), + destinationBounds.width(), + displayController, + ) if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) { // 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 @@ -902,7 +934,8 @@ class DesktopTasksController( position: SnapPosition, taskSurface: SurfaceControl, currentDragBounds: Rect, - dragStartBounds: Rect + dragStartBounds: Rect, + motionEvent: MotionEvent, ) { releaseVisualIndicator() if (!taskInfo.isResizeable && DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) { @@ -919,10 +952,25 @@ class DesktopTasksController( isResizable = taskInfo.isResizeable, ) } else { + val resizeTrigger = if (position == SnapPosition.LEFT) { + ResizeTrigger.DRAG_LEFT + } else { + ResizeTrigger.DRAG_RIGHT + } + desktopModeEventLogger.logTaskResizingStarted( + resizeTrigger, motionEvent, taskInfo, displayController + ) interactionJankMonitor.begin( taskSurface, context, handler, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable" ) - snapToHalfScreen(taskInfo, taskSurface, currentDragBounds, position) + snapToHalfScreen( + taskInfo, + taskSurface, + currentDragBounds, + position, + resizeTrigger, + motionEvent, + ) } } @@ -1587,12 +1635,26 @@ class DesktopTasksController( getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) } } + /** Move the focused desktop task in given `displayId` to next display. */ + fun moveFocusedTaskToNextDisplay(displayId: Int) { + getFocusedFreeformTask(displayId)?.let { moveToNextDisplay(it.taskId) } + } + private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? { return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM } } + // TODO(b/364154795): wait for the completion of moveToNextDisplay transition, otherwise it will + // pick a wrong task when a user quickly perform other actions with keyboard shortcuts after + // moveToNextDisplay. + private fun getGloballyFocusedFreeformTask(): RunningTaskInfo? = + shellTaskOrganizer.getRunningTasks().find { taskInfo -> + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM && + focusTransitionObserver.hasGlobalFocus(taskInfo) + } + /** * Requests a task be transitioned from desktop to split select. Applies needed windowing * changes if this transition is enabled. @@ -1708,6 +1770,7 @@ class DesktopTasksController( currentDragBounds: Rect, validDragArea: Rect, dragStartBounds: Rect, + motionEvent: MotionEvent, ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return @@ -1728,12 +1791,22 @@ class DesktopTasksController( } IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { handleSnapResizingTask( - taskInfo, SnapPosition.LEFT, taskSurface, currentDragBounds, dragStartBounds + taskInfo, + SnapPosition.LEFT, + taskSurface, + currentDragBounds, + dragStartBounds, + motionEvent, ) } IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { handleSnapResizingTask( - taskInfo, SnapPosition.RIGHT, taskSurface, currentDragBounds, dragStartBounds + taskInfo, + SnapPosition.RIGHT, + taskSurface, + currentDragBounds, + dragStartBounds, + motionEvent, ) } IndicatorType.NO_INDICATOR -> { @@ -1947,6 +2020,31 @@ class DesktopTasksController( taskRepository.dump(pw, innerPrefix) } + override fun handleKeyGestureEvent( + event: KeyGestureEvent, + focusedToken: IBinder? + ): Boolean { + if (!isKeyGestureSupported(event.keyGestureType)) return false + when (event.keyGestureType) { + KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY -> { + if (event.keycodes.contains(KeyEvent.KEYCODE_D) && + event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_META_ON)) { + logV("Key gesture MOVE_TO_NEXT_DISPLAY is handled") + getGloballyFocusedFreeformTask()?.let { moveToNextDisplay(it.taskId) } + return true + } + return false + } + else -> return false + } + } + + override fun isKeyGestureSupported(gestureType: Int): Boolean = when (gestureType) { + KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY + -> enableMoveToNextDisplayShortcut() + else -> false + } + /** The interface for calls from outside the shell, within the host process. */ @ExternalThread private inner class DesktopModeImpl : DesktopMode { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt index 3f41d7cf4e86..2d11e02bd3c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -73,15 +73,14 @@ class DesktopPersistentRepository( */ private suspend fun getDesktopRepositoryState( userId: Int = DEFAULT_USER_ID - ): DesktopRepositoryState = + ): DesktopRepositoryState? = try { dataStoreFlow .first() - .desktopRepoByUserMap - .getOrDefault(userId, DesktopRepositoryState.getDefaultInstance()) + .desktopRepoByUserMap[userId] } catch (e: Exception) { Log.e(TAG, "Unable to read from datastore", e) - DesktopRepositoryState.getDefaultInstance() + null } /** @@ -91,13 +90,13 @@ class DesktopPersistentRepository( suspend fun readDesktop( userId: Int = DEFAULT_USER_ID, desktopId: Int = DEFAULT_DESKTOP_ID, - ): Desktop = + ): Desktop? = try { val repository = getDesktopRepositoryState(userId) - repository.getDesktopOrThrow(desktopId) + repository?.getDesktopOrThrow(desktopId) } catch (e: Exception) { Log.e(TAG, "Unable to get desktop info from persistent repository", e) - Desktop.getDefaultInstance() + null } /** Adds or updates a desktop stored in the datastore */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 509cb85c96cd..fde01eefee17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -274,6 +274,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL closeDragResizeListener(); mDragResizeListener = new DragResizeInputListener( mContext, + mTaskInfo, mHandler, mChoreographer, mDisplay.getDisplayId(), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index a06b4a2e09d4..a775cbc6c9f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -32,6 +32,7 @@ import static android.view.WindowInsets.Type.statusBars; import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_MODE_APP_HANDLE_MENU; import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing; +import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR; @@ -103,6 +104,7 @@ import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; @@ -221,6 +223,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, }; private final TaskPositionerFactory mTaskPositionerFactory; private final FocusTransitionObserver mFocusTransitionObserver; + private final DesktopModeEventLogger mDesktopModeEventLogger; public DesktopModeWindowDecorViewModel( Context context, @@ -248,7 +251,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, - FocusTransitionObserver focusTransitionObserver) { + FocusTransitionObserver focusTransitionObserver, + DesktopModeEventLogger desktopModeEventLogger) { this( context, shellExecutor, @@ -281,7 +285,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory(), - focusTransitionObserver); + focusTransitionObserver, + desktopModeEventLogger); } @VisibleForTesting @@ -317,7 +322,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, TaskPositionerFactory taskPositionerFactory, - FocusTransitionObserver focusTransitionObserver) { + FocusTransitionObserver focusTransitionObserver, + DesktopModeEventLogger desktopModeEventLogger) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -378,6 +384,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, }; mTaskPositionerFactory = taskPositionerFactory; mFocusTransitionObserver = focusTransitionObserver; + mDesktopModeEventLogger = desktopModeEventLogger; shellInit.addInitCallback(this::onInit, this); } @@ -547,15 +554,20 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, >= MANAGE_WINDOWS_MINIMUM_INSTANCES); } - private void onMaximizeOrRestore(int taskId, String source) { + private void onMaximizeOrRestore(int taskId, String source, ResizeTrigger resizeTrigger, + MotionEvent motionEvent) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { return; } + mDesktopModeEventLogger.logTaskResizingStarted(resizeTrigger, motionEvent, + decoration.mTaskInfo, + mDisplayController, /* displayLayoutSize= */ null); mInteractionJankMonitor.begin( decoration.mTaskSurface, mContext, mMainHandler, Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, source); - mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); + mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo, resizeTrigger, + motionEvent); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); } @@ -568,7 +580,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo); } - private void onSnapResize(int taskId, boolean left) { + private void onSnapResize(int taskId, boolean left, MotionEvent motionEvent) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { return; @@ -579,13 +591,20 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Toast.makeText(mContext, R.string.desktop_mode_non_resizable_snap_text, Toast.LENGTH_SHORT).show(); } else { + ResizeTrigger resizeTrigger = + left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU; + mDesktopModeEventLogger.logTaskResizingStarted(resizeTrigger, motionEvent, + decoration.mTaskInfo, + mDisplayController, /* displayLayoutSize= */ null); mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, mMainHandler, Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable"); mDesktopTasksController.snapToHalfScreen( decoration.mTaskInfo, decoration.mTaskSurface, decoration.mTaskInfo.configuration.windowConfiguration.getBounds(), - left ? SnapPosition.LEFT : SnapPosition.RIGHT); + left ? SnapPosition.LEFT : SnapPosition.RIGHT, + resizeTrigger, + motionEvent); } decoration.closeHandleMenu(); @@ -737,6 +756,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private boolean mTouchscreenInUse; private boolean mHasLongClicked; private int mDragPointerId = -1; + private MotionEvent mMotionEvent; private DesktopModeTouchEventListener( RunningTaskInfo taskInfo, @@ -798,7 +818,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } else { // Full immersive is disabled or task doesn't request/support it, so just // toggle between maximize/restore states. - onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button"); + onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button", + ResizeTrigger.MAXIMIZE_BUTTON, mMotionEvent); } } else if (id == R.id.minimize_window) { mDesktopTasksController.minimizeTask(decoration.mTaskInfo); @@ -807,6 +828,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, @Override public boolean onTouch(View v, MotionEvent e) { + mMotionEvent = e; final int id = v.getId(); final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); if ((e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN) { @@ -897,6 +919,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, */ @Override public boolean onGenericMotion(View v, MotionEvent ev) { + mMotionEvent = ev; final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); if (ev.getAction() == ACTION_HOVER_ENTER && id == R.id.maximize_window) { @@ -1040,7 +1063,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, taskInfo, decoration.mTaskSurface, position, new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), newTaskBounds, decoration.calculateValidDragArea(), - new Rect(mOnDragStartInitialBounds)); + new Rect(mOnDragStartInitialBounds), e); if (touchingButton && !mHasLongClicked) { // We need the input event to not be consumed here to end the ripple // effect on the touched button. We will reset drag state in the ensuing @@ -1087,7 +1110,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // Disallow double-tap to resize when in full immersive. return false; } - onMaximizeOrRestore(mTaskId, "double_tap"); + onMaximizeOrRestore(mTaskId, "double_tap", ResizeTrigger.DOUBLE_TAP_APP_HEADER, e); return true; } } @@ -1484,7 +1507,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mGenericLinksParser, mAssistContentRequester, mMultiInstanceHelper, - mWindowDecorCaptionHandleRepository); + mWindowDecorCaptionHandleRepository, + mDesktopModeEventLogger); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); final TaskPositioner taskPositioner = mTaskPositionerFactory.create( @@ -1501,15 +1525,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, final DesktopModeTouchEventListener touchEventListener = new DesktopModeTouchEventListener(taskInfo, taskPositioner); windowDecoration.setOnMaximizeOrRestoreClickListener(() -> { - onMaximizeOrRestore(taskInfo.taskId, "maximize_menu"); + onMaximizeOrRestore(taskInfo.taskId, "maximize_menu", ResizeTrigger.MAXIMIZE_MENU, + touchEventListener.mMotionEvent); return Unit.INSTANCE; }); windowDecoration.setOnLeftSnapClickListener(() -> { - onSnapResize(taskInfo.taskId, true /* isLeft */); + onSnapResize(taskInfo.taskId, /* isLeft= */ true, touchEventListener.mMotionEvent); return Unit.INSTANCE; }); windowDecoration.setOnRightSnapClickListener(() -> { - onSnapResize(taskInfo.taskId, false /* isLeft */); + onSnapResize(taskInfo.taskId, /* isLeft= */ false, touchEventListener.mMotionEvent); return Unit.INSTANCE; }); windowDecoration.setOnToDesktopClickListener(desktopModeTransitionSource -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 6eb20b9e3ae5..d94f3a9a70c6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -94,6 +94,7 @@ import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.CaptionState; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; @@ -216,7 +217,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin AppToWebGenericLinksParser genericLinksParser, AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, - WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) { + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + DesktopModeEventLogger desktopModeEventLogger) { this (context, userContext, displayController, splitScreenController, desktopRepository, taskOrganizer, taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue, appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser, @@ -227,7 +229,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE, DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper, - windowDecorCaptionHandleRepository); + windowDecorCaptionHandleRepository, desktopModeEventLogger); } DesktopModeWindowDecoration( @@ -256,11 +258,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin MaximizeMenuFactory maximizeMenuFactory, HandleMenuFactory handleMenuFactory, MultiInstanceHelper multiInstanceHelper, - WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) { + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + DesktopModeEventLogger desktopModeEventLogger) { super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, - surfaceControlViewHostFactory); + surfaceControlViewHostFactory, desktopModeEventLogger); mSplitScreenController = splitScreenController; mHandler = handler; mBgExecutor = bgExecutor; @@ -605,6 +608,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Trace.beginSection("DesktopModeWindowDecoration#relayout-DragResizeInputListener"); mDragResizeListener = new DragResizeInputListener( mContext, + mTaskInfo, mHandler, mChoreographer, mDisplay.getDisplayId(), @@ -612,7 +616,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDragPositioningCallback, mSurfaceControlBuilderSupplier, mSurfaceControlTransactionSupplier, - mDisplayController); + mDisplayController, + mDesktopModeEventLogger); Trace.endSection(); } @@ -1700,7 +1705,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin AppToWebGenericLinksParser genericLinksParser, AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, - WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) { + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + DesktopModeEventLogger desktopModeEventLogger) { return new DesktopModeWindowDecoration( context, userContext, @@ -1719,7 +1725,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin genericLinksParser, assistContentRequester, multiInstanceHelper, - windowDecorCaptionHandleRepository); + windowDecorCaptionHandleRepository, + desktopModeEventLogger); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 4ff394e2b1a9..420409705b05 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -29,10 +29,12 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; +import static com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEventFromTouchscreen; import android.annotation.NonNull; +import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; @@ -59,6 +61,7 @@ import android.window.InputTransferToken; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import java.util.function.Consumer; import java.util.function.Supplier; @@ -83,14 +86,17 @@ class DragResizeInputListener implements AutoCloseable { private final TaskResizeInputEventReceiver mInputEventReceiver; private final Context mContext; + private final RunningTaskInfo mTaskInfo; private final SurfaceControl mInputSinkSurface; private final IBinder mSinkClientToken; private final InputChannel mSinkInputChannel; private final DisplayController mDisplayController; + private final DesktopModeEventLogger mDesktopModeEventLogger; private final Region mTouchRegion = new Region(); DragResizeInputListener( Context context, + RunningTaskInfo taskInfo, Handler handler, Choreographer choreographer, int displayId, @@ -98,12 +104,15 @@ class DragResizeInputListener implements AutoCloseable { DragPositioningCallback callback, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, - DisplayController displayController) { + DisplayController displayController, + DesktopModeEventLogger desktopModeEventLogger) { mContext = context; + mTaskInfo = taskInfo; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mDisplayId = displayId; mDecorationSurface = decorationSurface; mDisplayController = displayController; + mDesktopModeEventLogger = desktopModeEventLogger; mClientToken = new Binder(); final InputTransferToken inputTransferToken = new InputTransferToken(); mInputChannel = new InputChannel(); @@ -125,11 +134,12 @@ class DragResizeInputListener implements AutoCloseable { e.rethrowFromSystemServer(); } - mInputEventReceiver = new TaskResizeInputEventReceiver(context, mInputChannel, callback, + mInputEventReceiver = new TaskResizeInputEventReceiver(context, mTaskInfo, mInputChannel, + callback, handler, choreographer, () -> { final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId); return new Size(layout.width(), layout.height()); - }, this::updateSinkInputChannel); + }, this::updateSinkInputChannel, mDesktopModeEventLogger); mInputEventReceiver.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop()); mInputSinkSurface = surfaceControlBuilderSupplier.get() @@ -163,6 +173,22 @@ class DragResizeInputListener implements AutoCloseable { } } + DragResizeInputListener( + Context context, + RunningTaskInfo taskInfo, + Handler handler, + Choreographer choreographer, + int displayId, + SurfaceControl decorationSurface, + DragPositioningCallback callback, + Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, + Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, + DisplayController displayController) { + this(context, taskInfo, handler, choreographer, displayId, decorationSurface, callback, + surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, displayController, + new DesktopModeEventLogger()); + } + /** * Updates the geometry (the touch region) of this drag resize handler. * @@ -274,6 +300,7 @@ class DragResizeInputListener implements AutoCloseable { private static class TaskResizeInputEventReceiver extends InputEventReceiver implements DragDetector.MotionEventHandler { @NonNull private final Context mContext; + @NonNull private final RunningTaskInfo mTaskInfo; private final InputManager mInputManager; @NonNull private final InputChannel mInputChannel; @NonNull private final DragPositioningCallback mCallback; @@ -282,6 +309,7 @@ class DragResizeInputListener implements AutoCloseable { @NonNull private final DragDetector mDragDetector; @NonNull private final Supplier<Size> mDisplayLayoutSizeSupplier; @NonNull private final Consumer<Region> mTouchRegionConsumer; + @NonNull private final DesktopModeEventLogger mDesktopModeEventLogger; private final Rect mTmpRect = new Rect(); private boolean mConsumeBatchEventScheduled; private DragResizeWindowGeometry mDragResizeWindowGeometry; @@ -293,15 +321,24 @@ class DragResizeInputListener implements AutoCloseable { // resize events. For example, if multiple fingers are touching the screen, then each one // has a separate pointer id, but we only accept drag input from one. private int mDragPointerId = -1; + // The type of resizing that is currently being done. Used to track the same resize trigger + // on start and end of the resizing action. + private ResizeTrigger mResizeTrigger = ResizeTrigger.UNKNOWN_RESIZE_TRIGGER; + // The last MotionEvent on ACTION_DOWN, used to track the input tool type and source for + // logging the start and end of the resizing action. + private MotionEvent mLastMotionEventOnDown; private TaskResizeInputEventReceiver(@NonNull Context context, + @NonNull RunningTaskInfo taskInfo, @NonNull InputChannel inputChannel, @NonNull DragPositioningCallback callback, @NonNull Handler handler, @NonNull Choreographer choreographer, @NonNull Supplier<Size> displayLayoutSizeSupplier, - @NonNull Consumer<Region> touchRegionConsumer) { + @NonNull Consumer<Region> touchRegionConsumer, + @NonNull DesktopModeEventLogger desktopModeEventLogger) { super(inputChannel, handler.getLooper()); mContext = context; + mTaskInfo = taskInfo; mInputManager = context.getSystemService(InputManager.class); mInputChannel = inputChannel; mCallback = callback; @@ -322,6 +359,7 @@ class DragResizeInputListener implements AutoCloseable { ViewConfiguration.get(mContext).getScaledTouchSlop()); mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier; mTouchRegionConsumer = touchRegionConsumer; + mDesktopModeEventLogger = desktopModeEventLogger; } /** @@ -395,6 +433,7 @@ class DragResizeInputListener implements AutoCloseable { @Override public boolean handleMotionEvent(View v, MotionEvent e) { boolean result = false; + // Check if this is a touch event vs mouse event. // Touch events are tracked in four corners. Other events are tracked in resize edges. switch (e.getActionMasked()) { @@ -416,6 +455,13 @@ class DragResizeInputListener implements AutoCloseable { "%s: Handling action down, update ctrlType to %d", TAG, ctrlType); mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, rawX, rawY); + mLastMotionEventOnDown = e; + mResizeTrigger = (ctrlType == CTRL_TYPE_BOTTOM || ctrlType == CTRL_TYPE_TOP + || ctrlType == CTRL_TYPE_RIGHT || ctrlType == CTRL_TYPE_LEFT) + ? ResizeTrigger.EDGE : ResizeTrigger.CORNER; + mDesktopModeEventLogger.logTaskResizingStarted(mResizeTrigger, + e, mTaskInfo, /* displayController= */ null, + /* displayLayoutSize= */ mDisplayLayoutSizeSupplier.get()); // Increase the input sink region to cover the whole screen; this is to // prevent input and focus from going to other tasks during a drag resize. updateInputSinkRegionForDrag(mDragStartTaskBounds); @@ -464,6 +510,12 @@ class DragResizeInputListener implements AutoCloseable { if (taskBounds.equals(mDragStartTaskBounds)) { mTouchRegionConsumer.accept(mTouchRegion); } + + mDesktopModeEventLogger.logTaskResizingEnded(mResizeTrigger, + mLastMotionEventOnDown, mTaskInfo, taskBounds.height(), + taskBounds.width(), + /* displayController= */ null, + /* displayLayoutSize= */ mDisplayLayoutSizeSupplier.get()); } mShouldHandleEvents = false; mDragPointerId = -1; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java index 33d1c260cb84..844ceb304bde 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java @@ -181,7 +181,7 @@ final class DragResizeWindowGeometry { } private boolean isInEdgeResizeBounds(float x, float y) { - return calculateEdgeResizeCtrlType(x, y) != 0; + return calculateEdgeResizeCtrlType(x, y) != CTRL_TYPE_UNDEFINED; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 6b3b357f2f7b..34cc0986c83f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -58,6 +58,7 @@ import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer; @@ -111,6 +112,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final Context mContext; final @NonNull Context mUserContext; final @NonNull DisplayController mDisplayController; + final @NonNull DesktopModeEventLogger mDesktopModeEventLogger; final ShellTaskOrganizer mTaskOrganizer; final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; @@ -163,7 +165,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> this(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}); + new SurfaceControlViewHostFactory() {}, new DesktopModeEventLogger()); } WindowDecoration( @@ -177,13 +179,16 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, - SurfaceControlViewHostFactory surfaceControlViewHostFactory) { + SurfaceControlViewHostFactory surfaceControlViewHostFactory, + @NonNull DesktopModeEventLogger desktopModeEventLogger + ) { mContext = context; mUserContext = userContext; mDisplayController = displayController; mTaskOrganizer = taskOrganizer; mTaskInfo = taskInfo; mTaskSurface = cloneSurfaceControl(taskSurface, surfaceControlSupplier); + mDesktopModeEventLogger = desktopModeEventLogger; mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mWindowContainerTransactionSupplier = windowContainerTransactionSupplier; 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 b7ddfd1fc6eb..4fe66f3357a3 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 @@ -298,12 +298,18 @@ class DesktopModeFlickerScenarios { FlickerConfigEntry( scenarioId = ScenarioId("MAXIMIZE_APP"), extractor = - TaggedScenarioExtractorBuilder() - .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) - .setTransitionMatcher( - TaggedCujTransitionMatcher(associatedTransitionRequired = false) - ) - .build(), + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.DESKTOP_MODE_TOGGLE_RESIZE + } + } + } + ), assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + listOf( AppLayerIncreasesInSize(DESKTOP_MODE_APP), @@ -316,12 +322,18 @@ class DesktopModeFlickerScenarios { FlickerConfigEntry( scenarioId = ScenarioId("MAXIMIZE_APP_NON_RESIZABLE"), extractor = - TaggedScenarioExtractorBuilder() - .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) - .setTransitionMatcher( - TaggedCujTransitionMatcher(associatedTransitionRequired = false) - ) - .build(), + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.DESKTOP_MODE_TOGGLE_RESIZE + } + } + } + ), assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + listOf( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java index e6bd05b82be9..f935ac76bbeb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java @@ -40,6 +40,8 @@ public final class TestRunningTaskInfoBuilder { private WindowContainerToken mToken = createMockWCToken(); private int mParentTaskId = INVALID_TASK_ID; + private int mUid = INVALID_TASK_ID; + private int mTaskId = INVALID_TASK_ID; private Intent mBaseIntent = new Intent(); private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD; private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED; @@ -73,6 +75,18 @@ public final class TestRunningTaskInfoBuilder { return this; } + /** Sets the task info's effective UID. */ + public TestRunningTaskInfoBuilder setUid(int uid) { + mUid = uid; + return this; + } + + /** Sets the task info's UID. */ + public TestRunningTaskInfoBuilder setTaskId(int taskId) { + mTaskId = taskId; + return this; + } + /** * Set {@link ActivityManager.RunningTaskInfo#baseIntent} for the task info, by default * an empty intent is assigned @@ -132,7 +146,8 @@ public final class TestRunningTaskInfoBuilder { public ActivityManager.RunningTaskInfo build() { final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); - info.taskId = sNextTaskId++; + info.taskId = (mTaskId == INVALID_TASK_ID) ? sNextTaskId++ : mTaskId; + info.effectiveUid = mUid; info.baseIntent = mBaseIntent; info.parentTaskId = mParentTaskId; info.displayId = mDisplayId; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index 0825b6b0d7be..2a82e6e4f7b8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -16,9 +16,12 @@ package com.android.wm.shell.desktopmode +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker import com.android.dx.mockito.inline.extended.ExtendedMockito.verify import com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions @@ -27,6 +30,9 @@ import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags import com.android.wm.shell.EventLogTags import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod @@ -39,9 +45,13 @@ import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UNSET_M import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UNSET_UNMINIMIZE_REASON import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever /** * Tests for [DesktopModeEventLogger]. @@ -49,6 +59,8 @@ import org.mockito.kotlin.eq class DesktopModeEventLoggerTest : ShellTestCase() { private val desktopModeEventLogger = DesktopModeEventLogger() + val displayController = mock<DisplayController>() + val displayLayout = mock<DisplayLayout>() @JvmField @Rule(order = 0) @@ -60,6 +72,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { @Rule(order = 1) val setFlagsRule = SetFlagsRule() + @Before + fun setUp() { + doReturn(displayLayout).whenever(displayController).getDisplayLayout(anyInt()) + doReturn(DISPLAY_WIDTH).whenever(displayLayout).width() + doReturn(DISPLAY_HEIGHT).whenever(displayLayout).height() + } + @Test fun logSessionEnter_logsEnterReasonWithNewSessionId() { desktopModeEventLogger.logSessionEnter(EnterReason.KEYBOARD_SHORTCUT_ENTER) @@ -467,7 +486,8 @@ class DesktopModeEventLoggerTest : ShellTestCase() { @Test fun logTaskResizingStarted_noOngoingSession_doesNotLog() { - desktopModeEventLogger.logTaskResizingStarted(TASK_SIZE_UPDATE) + desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER, + null, createTaskInfo()) verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -478,13 +498,14 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskResizingStarted_logsTaskSizeUpdatedWithStartResizingStage() { val sessionId = startDesktopModeSession() - desktopModeEventLogger.logTaskResizingStarted(TASK_SIZE_UPDATE) + desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER, + null, createTaskInfo(), displayController) verify { FrameworkStatsLog.write( eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), /* resize_trigger */ - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__UNKNOWN_RESIZE_TRIGGER), + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER), /* resizing_stage */ eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE), /* input_method */ @@ -500,7 +521,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { /* task_width */ eq(TASK_SIZE_UPDATE.taskWidth), /* display_area */ - eq(TASK_SIZE_UPDATE.displayArea), + eq(DISPLAY_AREA), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) @@ -508,7 +529,8 @@ class DesktopModeEventLoggerTest : ShellTestCase() { @Test fun logTaskResizingEnded_noOngoingSession_doesNotLog() { - desktopModeEventLogger.logTaskResizingEnded(TASK_SIZE_UPDATE) + desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER, + null, createTaskInfo()) verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -519,13 +541,14 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskResizingEnded_logsTaskSizeUpdatedWithEndResizingStage() { val sessionId = startDesktopModeSession() - desktopModeEventLogger.logTaskResizingEnded(TASK_SIZE_UPDATE) + desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER, + null, createTaskInfo(), displayController = displayController) verify { FrameworkStatsLog.write( eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), /* resize_trigger */ - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__UNKNOWN_RESIZE_TRIGGER), + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER), /* resizing_stage */ eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE), /* input_method */ @@ -541,7 +564,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { /* task_width */ eq(TASK_SIZE_UPDATE.taskWidth), /* display_area */ - eq(TASK_SIZE_UPDATE.displayArea), + eq(DISPLAY_AREA), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) @@ -585,8 +608,14 @@ class DesktopModeEventLoggerTest : ShellTestCase() { } } + private fun createTaskInfo(): RunningTaskInfo { + return TestRunningTaskInfoBuilder().setTaskId(TASK_ID) + .setUid(TASK_UID) + .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT)) + .build() + } + private companion object { - private const val sessionId = 1 private const val TASK_ID = 1 private const val TASK_UID = 1 private const val TASK_X = 0 @@ -594,7 +623,9 @@ class DesktopModeEventLoggerTest : ShellTestCase() { private const val TASK_HEIGHT = 100 private const val TASK_WIDTH = 100 private const val TASK_COUNT = 1 - private const val DISPLAY_AREA = 1000 + private const val DISPLAY_WIDTH = 500 + private const val DISPLAY_HEIGHT = 500 + private const val DISPLAY_AREA = DISPLAY_HEIGHT * DISPLAY_WIDTH private val TASK_UPDATE = TaskUpdate( TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y, 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 af51e32b2086..7c336cdb54f6 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 @@ -39,6 +39,9 @@ import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.graphics.Point import android.graphics.PointF import android.graphics.Rect +import android.hardware.input.InputManager +import android.hardware.input.InputManager.KeyGestureEventHandler +import android.hardware.input.KeyGestureEvent import android.os.Binder import android.os.Bundle import android.os.Handler @@ -50,6 +53,8 @@ import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.Gravity +import android.view.KeyEvent +import android.view.MotionEvent import android.view.SurfaceControl import android.view.WindowInsets import android.view.WindowManager @@ -70,14 +75,18 @@ import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_R import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import android.window.WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP +import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT import com.android.wm.shell.MockToken import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer @@ -91,6 +100,7 @@ import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask @@ -112,6 +122,7 @@ import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.TestRemoteTransition import com.android.wm.shell.transition.Transitions @@ -205,7 +216,11 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter @Mock private lateinit var mockHandler: Handler + @Mock private lateinit var desktopModeEventLogger: DesktopModeEventLogger @Mock lateinit var persistentRepository: DesktopPersistentRepository + @Mock private lateinit var mockInputManager: InputManager + @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver + @Mock lateinit var motionEvent: MotionEvent private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController @@ -214,6 +229,7 @@ class DesktopTasksControllerTest : ShellTestCase() { private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener private lateinit var testScope: CoroutineScope + private lateinit var keyGestureEventHandler: KeyGestureEventHandler private val shellExecutor = TestShellExecutor() @@ -271,6 +287,11 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.setSplitScreenController(splitScreenController) controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter + doAnswer { + keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler) + null + }.whenever(mockInputManager).registerKeyGestureEventHandler(any()) + shellInit.init() val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) @@ -278,6 +299,8 @@ class DesktopTasksControllerTest : ShellTestCase() { recentsTransitionStateListener = captor.value controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener + + assumeTrue(ENABLE_SHELL_TRANSITIONS) } private fun createController(): DesktopTasksController { @@ -310,6 +333,9 @@ class DesktopTasksControllerTest : ShellTestCase() { recentTasksController, mockInteractionJankMonitor, mockHandler, + mockInputManager, + mockFocusTransitionObserver, + desktopModeEventLogger, ) } @@ -338,9 +364,17 @@ class DesktopTasksControllerTest : ShellTestCase() { val task1 = setUpFreeformTask() val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) - controller.toggleDesktopTaskSize(task1) - verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + controller.toggleDesktopTaskSize(task1, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) + verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + motionEvent, + task1, + STABLE_BOUNDS.height(), + STABLE_BOUNDS.width(), + displayController + ) assertThat(argumentCaptor.value).isTrue() } @@ -357,9 +391,17 @@ class DesktopTasksControllerTest : ShellTestCase() { val task1 = setUpFreeformTask(bounds = stableBounds, active = true) val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) - controller.toggleDesktopTaskSize(task1) - verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + controller.toggleDesktopTaskSize(task1, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) + verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + motionEvent, + task1, + 0, + 0, + displayController + ) assertThat(argumentCaptor.value).isFalse() } @@ -735,7 +777,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) fun handleRequest_newFreeformTaskLaunch_cascadeApplied() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) setUpLandscapeDisplay() val stableBounds = Rect() displayLayout.getStableBoundsForDesktopMode(stableBounds) @@ -754,7 +795,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) fun handleRequest_freeformTaskAlreadyExistsInDesktopMode_cascadeNotApplied() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) setUpLandscapeDisplay() val stableBounds = Rect() displayLayout.getStableBoundsForDesktopMode(stableBounds) @@ -1467,6 +1507,44 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS, + FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, + FLAG_USE_KEY_GESTURE_EVENT_HANDLER + ) + fun moveToNextDisplay_withKeyGesture() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: default display + val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultDisplayArea) + // Setup a focused task on secondary display, which is expected to move to default display + val task = setUpFreeformTask(displayId = SECOND_DISPLAY) + task.isFocused = true + whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task)) + whenever(mockFocusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true) + + val event = KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY) + .setDisplayId(SECOND_DISPLAY) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON) + .build() + val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) + + assertThat(result).isTrue() + with(getLatestWct(type = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + + @Test fun getTaskWindowingMode() { val fullscreenTask = setUpFullscreenTask() val freeformTask = setUpFreeformTask() @@ -1716,8 +1794,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -1734,8 +1810,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) @@ -1761,8 +1835,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTask = setUpFreeformTask() markTaskVisible(freeformTask) val fullscreenTask = createFullscreenTask() @@ -1776,8 +1848,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() @@ -1792,8 +1862,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val fullscreenTask = createFullscreenTask() @@ -1809,8 +1877,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val minimizedTask = setUpFreeformTask() taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId) val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } @@ -1831,7 +1897,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -1848,7 +1913,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN @@ -1861,8 +1925,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTask = setUpFreeformTask() markTaskHidden(freeformTask) val fullscreenTask = createFullscreenTask() @@ -1871,16 +1933,12 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val fullscreenTask = createFullscreenTask() assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() } @Test fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) createFreeformTask(displayId = SECOND_DISPLAY) @@ -1890,8 +1948,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } freeformTasks.forEach { markTaskVisible(it) } val newFreeformTask = createFreeformTask() @@ -1904,8 +1960,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTask = setUpFreeformTask() markTaskHidden(freeformTask) @@ -1920,7 +1974,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -1935,7 +1988,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN @@ -1952,8 +2004,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -1969,8 +2019,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -1991,8 +2039,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val task = createFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task)) @@ -2004,8 +2050,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val task = createFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task)) @@ -2020,8 +2064,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task createFreeformTask(displayId = SECOND_DISPLAY) @@ -2036,8 +2078,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task createFreeformTask(displayId = SECOND_DISPLAY) @@ -2054,7 +2094,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false) val freeformTask1 = setUpFreeformTask() @@ -2068,7 +2107,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true) val freeformTask1 = setUpFreeformTask() @@ -2082,7 +2120,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_freeformTask_keyguardLocked_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(keyguardManager.isKeyguardLocked).thenReturn(true) val freeformTask = createFreeformTask(displayId = DEFAULT_DISPLAY) @@ -2093,8 +2130,6 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_notOpenOrToFrontTransition_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val task = TestRunningTaskInfoBuilder() .setActivityType(ACTIVITY_TYPE_STANDARD) @@ -2107,21 +2142,17 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun handleRequest_noTriggerTask_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull() } @Test fun handleRequest_triggerTaskNotStandard_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() } @Test fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) - val task = TestRunningTaskInfoBuilder() .setActivityType(ACTIVITY_TYPE_STANDARD) @@ -2803,7 +2834,8 @@ class DesktopTasksControllerTest : ShellTestCase() { PointF(200f, -200f), /* inputCoordinate */ Rect(100, -100, 500, 1000), /* currentDragBounds */ Rect(0, 50, 2000, 2000), /* validDragArea */ - Rect() /* dragStartBounds */ ) + Rect() /* dragStartBounds */, + motionEvent) val rectAfterEnd = Rect(100, 50, 500, 1150) verify(transitions) .startTransition( @@ -2838,7 +2870,8 @@ class DesktopTasksControllerTest : ShellTestCase() { PointF(200f, 300f), /* inputCoordinate */ currentDragBounds, /* currentDragBounds */ Rect(0, 50, 2000, 2000) /* validDragArea */, - Rect() /* dragStartBounds */) + Rect() /* dragStartBounds */, + motionEvent) verify(transitions) @@ -3085,10 +3118,19 @@ class DesktopTasksControllerTest : ShellTestCase() { val bounds = Rect(0, 0, 100, 100) val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) + // Assert bounds set to stable bounds val wct = getLatestToggleResizeDesktopTaskWct() assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + motionEvent, + task, + STABLE_BOUNDS.height(), + STABLE_BOUNDS.width(), + displayController + ) } @Test @@ -3107,15 +3149,22 @@ class DesktopTasksControllerTest : ShellTestCase() { STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom ) - controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT) + controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent) // Assert bounds set to stable bounds val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.SNAP_LEFT_MENU, + motionEvent, + task, + expectedBounds.height(), + expectedBounds.width(), + displayController + ) } @Test fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() { - assumeTrue(ENABLE_SHELL_TRANSITIONS) // Set up task to already be in snapped-left bounds val bounds = Rect( STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom @@ -3130,7 +3179,7 @@ class DesktopTasksControllerTest : ShellTestCase() { // Attempt to snap left again val currentDragBounds = Rect(bounds).apply { offset(-100, 0) } - controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT) + controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent) // Assert that task is NOT updated via WCT verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) @@ -3143,6 +3192,14 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(bounds), eq(true) ) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.SNAP_LEFT_MENU, + motionEvent, + task, + bounds.height(), + bounds.width(), + displayController + ) } @Test @@ -3153,12 +3210,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } val preDragBounds = Rect(100, 100, 400, 500) val currentDragBounds = Rect(0, 100, 300, 500) + val expectedBounds = + Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom) controller.handleSnapResizingTask( - task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds) + task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent + ) val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) assertThat(findBoundsChange(wct, task)).isEqualTo( - Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom)) + expectedBounds + ) + verify(desktopModeEventLogger, times(1)).logTaskResizingStarted( + ResizeTrigger.DRAG_LEFT, + motionEvent, + task, + displayController + ) } @Test @@ -3171,7 +3238,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val currentDragBounds = Rect(0, 100, 300, 500) controller.handleSnapResizingTask( - task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds) + task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent) verify(mReturnToDragStartAnimator).start( eq(task.taskId), eq(mockSurface), @@ -3179,6 +3246,13 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(preDragBounds), eq(false) ) + verify(desktopModeEventLogger, never()).logTaskResizingStarted( + any(), + any(), + any(), + any(), + any() + ) } @Test @@ -3197,10 +3271,19 @@ class DesktopTasksControllerTest : ShellTestCase() { // Bounds should be 1000 x 500, vertically centered in the 1000 x 1000 stable bounds val expectedBounds = Rect(STABLE_BOUNDS.left, 250, STABLE_BOUNDS.right, 750) - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) + // Assert bounds set to stable bounds val wct = getLatestToggleResizeDesktopTaskWct() assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + motionEvent, + task, + expectedBounds.height(), + expectedBounds.width(), + displayController + ) } @Test @@ -3208,8 +3291,12 @@ class DesktopTasksControllerTest : ShellTestCase() { val bounds = Rect(0, 0, 100, 100) val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds) + verify(desktopModeEventLogger, never()).logTaskResizingEnded( + any(), any(), any(), any(), + any(), any(), any() + ) } @Test @@ -3218,15 +3305,23 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) // Maximize - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) // Restore - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) // Assert bounds set to last bounds before maximize val wct = getLatestToggleResizeDesktopTaskWct() assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + motionEvent, + task, + boundsBeforeMaximize.height(), + boundsBeforeMaximize.width(), + displayController + ) } @Test @@ -3237,16 +3332,24 @@ class DesktopTasksControllerTest : ShellTestCase() { } // Maximize - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS.left, boundsBeforeMaximize.top, STABLE_BOUNDS.right, boundsBeforeMaximize.bottom) // Restore - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) // Assert bounds set to last bounds before maximize val wct = getLatestToggleResizeDesktopTaskWct() assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + motionEvent, + task, + boundsBeforeMaximize.height(), + boundsBeforeMaximize.width(), + displayController + ) } @Test @@ -3257,16 +3360,24 @@ class DesktopTasksControllerTest : ShellTestCase() { } // Maximize - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) task.configuration.windowConfiguration.bounds.set(boundsBeforeMaximize.left, STABLE_BOUNDS.top, boundsBeforeMaximize.right, STABLE_BOUNDS.bottom) // Restore - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) // Assert bounds set to last bounds before maximize val wct = getLatestToggleResizeDesktopTaskWct() assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + motionEvent, + task, + boundsBeforeMaximize.height(), + boundsBeforeMaximize.width(), + displayController + ) } @Test @@ -3275,14 +3386,22 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) // Maximize - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) // Restore - controller.toggleDesktopTaskSize(task) + controller.toggleDesktopTaskSize(task, ResizeTrigger.MAXIMIZE_BUTTON, motionEvent) // Assert last bounds before maximize removed after use assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() + verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + motionEvent, + task, + boundsBeforeMaximize.height(), + boundsBeforeMaximize.width(), + displayController + ) } @@ -3681,14 +3800,11 @@ class DesktopTasksControllerTest : ShellTestCase() { handlerClass: Class<out TransitionHandler>? = null ): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - if (handlerClass == null) { - verify(transitions).startTransition(eq(type), arg.capture(), isNull()) - } else { - verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) - } + + if (handlerClass == null) { + verify(transitions).startTransition(eq(type), arg.capture(), isNull()) } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) + verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) } return arg.value } @@ -3698,43 +3814,27 @@ class DesktopTasksControllerTest : ShellTestCase() { ): WindowContainerTransaction { val arg: ArgumentCaptor<WindowContainerTransaction> = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) + verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) .startTransition(capture(arg), eq(currentBounds)) - } else { - verify(shellTaskOrganizer).applyTransaction(capture(arg)) - } return arg.value } private fun getLatestEnterDesktopWct(): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) - } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) - } + verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) return arg.value } private fun getLatestDragToDesktopWct(): WindowContainerTransaction { val arg: ArgumentCaptor<WindowContainerTransaction> = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) - } else { - verify(shellTaskOrganizer).applyTransaction(capture(arg)) - } + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) return arg.value } private fun getLatestExitDesktopWct(): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (ENABLE_SHELL_TRANSITIONS) { - verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) - } else { - verify(shellTaskOrganizer).applyTransaction(arg.capture()) - } + verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) return arg.value } @@ -3742,27 +3842,15 @@ class DesktopTasksControllerTest : ShellTestCase() { wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds private fun verifyWCTNotExecuted() { - if (ENABLE_SHELL_TRANSITIONS) { - verify(transitions, never()).startTransition(anyInt(), any(), isNull()) - } else { - verify(shellTaskOrganizer, never()).applyTransaction(any()) - } + verify(transitions, never()).startTransition(anyInt(), any(), isNull()) } private fun verifyExitDesktopWCTNotExecuted() { - if (ENABLE_SHELL_TRANSITIONS) { - verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any()) - } else { - verify(shellTaskOrganizer, never()).applyTransaction(any()) - } + verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any()) } private fun verifyEnterDesktopWCTNotExecuted() { - if (ENABLE_SHELL_TRANSITIONS) { - verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any()) - } else { - verify(shellTaskOrganizer, never()).applyTransaction(any()) - } + verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any()) } private fun createTransition( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt index 9b9703fdf6dc..8495580f42a5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt @@ -115,8 +115,8 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { freeformTasksInZOrder = freeformTasksInZOrder) val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) - assertThat(actualDesktop.tasksByTaskIdMap).hasSize(2) - assertThat(actualDesktop.getZOrderedTasks(0)).isEqualTo(2) + assertThat(actualDesktop?.tasksByTaskIdMap).hasSize(2) + assertThat(actualDesktop?.getZOrderedTasks(0)).isEqualTo(2) } } @@ -138,7 +138,7 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { freeformTasksInZOrder = freeformTasksInZOrder) val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) - assertThat(actualDesktop.tasksByTaskIdMap[task.taskId]?.desktopTaskState) + assertThat(actualDesktop?.tasksByTaskIdMap?.get(task.taskId)?.desktopTaskState) .isEqualTo(DesktopTaskState.MINIMIZED) } } @@ -161,8 +161,8 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { freeformTasksInZOrder = freeformTasksInZOrder) val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) - assertThat(actualDesktop.tasksByTaskIdMap).isEmpty() - assertThat(actualDesktop.zOrderedTasksList).isEmpty() + assertThat(actualDesktop?.tasksByTaskIdMap).isEmpty() + assertThat(actualDesktop?.zOrderedTasksList).isEmpty() } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 175fbd2396e3..1839b8a367fe 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -87,6 +87,8 @@ import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler +import com.android.wm.shell.desktopmode.DesktopModeEventLogger +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition @@ -194,7 +196,11 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository + @Mock private lateinit var motionEvent: MotionEvent + @Mock lateinit var displayController: DisplayController + @Mock lateinit var displayLayout: DisplayLayout private lateinit var spyContext: TestableContext + private lateinit var desktopModeEventLogger: DesktopModeEventLogger private val transactionFactory = Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() @@ -224,6 +230,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { shellInit = ShellInit(mockShellExecutor) windowDecorByTaskIdSpy.clear() spyContext.addMockSystemService(InputManager::class.java, mockInputManager) + desktopModeEventLogger = mock<DesktopModeEventLogger>() desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( spyContext, mockShellExecutor, @@ -256,7 +263,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory, - mockFocusTransitionObserver + mockFocusTransitionObserver, + desktopModeEventLogger ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -299,6 +307,10 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { argumentCaptor<DesktopModeKeyguardChangeListener>() verify(mockShellController).addKeyguardChangeListener(keyguardChangedCaptor.capture()) desktopModeOnKeyguardChangedListener = keyguardChangedCaptor.firstValue + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } } @After @@ -612,7 +624,11 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { maxOrRestoreListenerCaptor.value.invoke() - verify(mockDesktopTasksController).toggleDesktopTaskSize(decor.mTaskInfo) + verify(mockDesktopTasksController).toggleDesktopTaskSize( + decor.mTaskInfo, + ResizeTrigger.MAXIMIZE_MENU, + null + ) } @Test @@ -647,7 +663,9 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { eq(decor.mTaskInfo), taskSurfaceCaptor.capture(), eq(currentBounds), - eq(SnapPosition.LEFT) + eq(SnapPosition.LEFT), + eq(ResizeTrigger.SNAP_LEFT_MENU), + eq(null) ) assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface) } @@ -685,7 +703,9 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { eq(decor.mTaskInfo), taskSurfaceCaptor.capture(), eq(currentBounds), - eq(SnapPosition.LEFT) + eq(SnapPosition.LEFT), + eq(ResizeTrigger.SNAP_LEFT_MENU), + eq(null) ) assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @@ -704,7 +724,9 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onLeftSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT)) + .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), + eq(ResizeTrigger.MAXIMIZE_BUTTON), + eq(null)) verify(mockToast).show() } @@ -725,7 +747,9 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { eq(decor.mTaskInfo), taskSurfaceCaptor.capture(), eq(currentBounds), - eq(SnapPosition.RIGHT) + eq(SnapPosition.RIGHT), + eq(ResizeTrigger.SNAP_RIGHT_MENU), + eq(null) ) assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @@ -763,7 +787,9 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { eq(decor.mTaskInfo), taskSurfaceCaptor.capture(), eq(currentBounds), - eq(SnapPosition.RIGHT) + eq(SnapPosition.RIGHT), + eq(ResizeTrigger.SNAP_RIGHT_MENU), + eq(null) ) assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @@ -782,7 +808,9 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onRightSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT)) + .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), + eq(ResizeTrigger.MAXIMIZE_BUTTON), + eq(null)) verify(mockToast).show() } @@ -1247,7 +1275,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onClickListenerCaptor.value.onClick(view) verify(mockDesktopTasksController) - .toggleDesktopTaskSize(decor.mTaskInfo) + .toggleDesktopTaskSize(decor.mTaskInfo, ResizeTrigger.MAXIMIZE_BUTTON, null) } private fun createOpenTaskDecoration( @@ -1337,7 +1365,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { whenever( mockDesktopModeWindowDecorFactory.create( any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), - any(), any(), any(), any(), any(), any(), any()) + any(), any(), any(), any(), any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.user).thenReturn(mockUserHandle) @@ -1378,5 +1406,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { private const val TAG = "DesktopModeWindowDecorViewModelTests" private val STABLE_INSETS = Rect(0, 100, 0, 0) private val INITIAL_BOUNDS = Rect(0, 0, 100, 100) + private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 320887212f54..0afb6c10b549 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -106,6 +106,7 @@ import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.CaptionState; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; @@ -210,6 +211,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private MultiInstanceHelper mMockMultiInstanceHelper; @Mock private WindowDecorCaptionHandleRepository mMockCaptionHandleRepository; + @Mock + private DesktopModeEventLogger mDesktopModeEventLogger; @Captor private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener; @Captor @@ -1400,7 +1403,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory, maximizeMenuFactory, mMockHandleMenuFactory, - mMockMultiInstanceHelper, mMockCaptionHandleRepository); + mMockMultiInstanceHelper, mMockCaptionHandleRepository, mDesktopModeEventLogger); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index fb17ae93030d..cb7fadee9822 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -83,6 +83,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.tests.R; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer; @@ -138,6 +139,8 @@ public class WindowDecorationTests extends ShellTestCase { private SurfaceSyncGroup mMockSurfaceSyncGroup; @Mock private SurfaceControl mMockTaskSurface; + @Mock + private DesktopModeEventLogger mDesktopModeEventLogger; private final List<SurfaceControl.Transaction> mMockSurfaceControlTransactions = new ArrayList<>(); @@ -1014,7 +1017,7 @@ public class WindowDecorationTests extends ShellTestCase { new MockObjectSupplier<>(mMockSurfaceControlTransactions, () -> mock(SurfaceControl.Transaction.class)), () -> mMockWindowContainerTransaction, () -> mMockTaskSurface, - mMockSurfaceControlViewHostFactory); + mMockSurfaceControlViewHostFactory, mDesktopModeEventLogger); } private class MockObjectSupplier<T> implements Supplier<T> { @@ -1054,11 +1057,12 @@ public class WindowDecorationTests extends ShellTestCase { Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, - SurfaceControlViewHostFactory surfaceControlViewHostFactory) { + SurfaceControlViewHostFactory surfaceControlViewHostFactory, + DesktopModeEventLogger desktopModeEventLogger) { super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, - surfaceControlViewHostFactory); + surfaceControlViewHostFactory, desktopModeEventLogger); } @Override diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 23097f634464..c7a7ed2a885e 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -17,7 +17,6 @@ #include "Color.h" #include <Properties.h> -#include <aidl/android/hardware/graphics/common/Dataspace.h> #include <android/hardware_buffer.h> #include <android/native_window.h> #include <ui/ColorSpace.h> @@ -222,8 +221,7 @@ android_dataspace ColorSpaceToADataSpace(SkColorSpace* colorSpace, SkColorType c if (nearlyEqual(fn, SkNamedTransferFn::kRec2020)) { return HAL_DATASPACE_BT2020; } else if (nearlyEqual(fn, SkNamedTransferFn::kSRGB)) { - return static_cast<android_dataspace>( - ::aidl::android::hardware::graphics::common::Dataspace::DISPLAY_BT2020); + return static_cast<android_dataspace>(HAL_DATASPACE_DISPLAY_BT2020); } } diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml index 2e3ee32e2efb..e3f8fbb88a65 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v31/settingslib_main_switch_bar.xml @@ -20,6 +20,8 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:minHeight="?android:attr/listPreferredItemHeight" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingTop="@dimen/settingslib_switchbar_margin" android:paddingBottom="@dimen/settingslib_switchbar_margin" android:orientation="vertical"> diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml index 3e0e18488f36..255b2c92e709 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v33/settingslib_main_switch_bar.xml @@ -20,6 +20,8 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:minHeight="?android:attr/listPreferredItemHeight" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingTop="@dimen/settingslib_switchbar_margin" android:paddingBottom="@dimen/settingslib_switchbar_margin" android:orientation="vertical"> diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml new file mode 100644 index 000000000000..94c6924a02f2 --- /dev/null +++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_layout.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:importantForAccessibility="no"> + + <com.android.settingslib.widget.MainSwitchBar + android:id="@+id/settingslib_main_switch_bar" + android:visibility="gone" + android:layout_height="wrap_content" + android:layout_width="match_parent" /> + +</FrameLayout> + + diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml index 7c0eaeaca3de..bf34db93298b 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml @@ -18,7 +18,11 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="wrap_content" - android:layout_width="match_parent"> + android:layout_width="match_parent" + android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingRight="?android:attr/listPreferredItemPaddingRight" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> <TextView android:id="@+id/switch_text" diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml index fa908a4ed6c8..bef6e352d854 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_layout.xml @@ -18,10 +18,6 @@ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="wrap_content" android:layout_width="match_parent" - android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" - android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingRight="?android:attr/listPreferredItemPaddingRight" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:importantForAccessibility="no"> <com.android.settingslib.widget.MainSwitchBar 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 3394874797e3..83858d9c9c54 100644 --- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java +++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java @@ -81,7 +81,11 @@ public class MainSwitchPreference extends TwoStatePreference } private void init(Context context, AttributeSet attrs) { - setLayoutResource(R.layout.settingslib_main_switch_layout); + boolean isExpressive = SettingsThemeHelper.isExpressiveTheme(context); + int resId = isExpressive + ? R.layout.settingslib_expressive_main_switch_layout + : R.layout.settingslib_main_switch_layout; + setLayoutResource(resId); mSwitchChangeListeners.add(this); if (attrs != null) { final TypedArray a = context.obtainStyledAttributes(attrs, diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt index ad996c7c8f86..b64f5dc49b4b 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt @@ -38,3 +38,11 @@ constructor( @StringRes override val title: Int = 0, @StringRes override val summary: Int = 0, ) : TwoStatePreference + +/** A preference that provides a two-state toggleable option that can be used as a main switch. */ +open class MainSwitchPreference +@JvmOverloads +constructor( + override val key: String, + @StringRes override val title: Int = 0, +) : TwoStatePreference
\ No newline at end of file diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp index bff95ceb137e..fb06be908733 100644 --- a/packages/SettingsLib/Preference/Android.bp +++ b/packages/SettingsLib/Preference/Android.bp @@ -32,6 +32,7 @@ android_library { static_libs: [ "SettingsLibDataStore", "SettingsLibMetadata", + "SettingsLibMainSwitchPreference", "androidx.annotation_annotation", "androidx.preference_preference", "guava", diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt index 4c2e1ba683f6..43f2cb6e2134 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt @@ -16,6 +16,7 @@ package com.android.settingslib.preference +import com.android.settingslib.metadata.MainSwitchPreference import com.android.settingslib.metadata.PreferenceGroup import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.SwitchPreference @@ -36,6 +37,7 @@ object DefaultPreferenceBindingFactory : PreferenceBindingFactory { is SwitchPreference -> SwitchPreferenceBinding.INSTANCE is PreferenceGroup -> PreferenceGroupBinding.INSTANCE is PreferenceScreenCreator -> PreferenceScreenBinding.INSTANCE + is MainSwitchPreference -> MainSwitchPreferenceBinding.INSTANCE else -> DefaultPreferenceBinding } } diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt index ede970e42e72..d40a6f6c55f4 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt @@ -21,11 +21,13 @@ import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat +import androidx.preference.TwoStatePreference import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceScreenMetadata import com.android.settingslib.metadata.PreferenceTitleProvider +import com.android.settingslib.widget.MainSwitchPreference /** Binding of preference group associated with [PreferenceCategory]. */ interface PreferenceScreenBinding : PreferenceBinding { @@ -64,23 +66,37 @@ interface PreferenceGroupBinding : PreferenceBinding { } } -/** A boolean value type preference associated with [SwitchPreferenceCompat]. */ -interface SwitchPreferenceBinding : PreferenceBinding { - - override fun createWidget(context: Context): Preference = SwitchPreferenceCompat(context) +/** A boolean value type preference associated with the abstract [TwoStatePreference]. */ +interface TwoStatePreferenceBinding : PreferenceBinding { override fun bind(preference: Preference, metadata: PreferenceMetadata) { super.bind(preference, metadata) (metadata as? PersistentPreference<*>) ?.storage(preference.context) ?.getValue(metadata.key, Boolean::class.javaObjectType) - ?.let { (preference as SwitchPreferenceCompat).isChecked = it } + ?.let { (preference as TwoStatePreference).isChecked = it } } +} + +/** A boolean value type preference associated with [SwitchPreferenceCompat]. */ +interface SwitchPreferenceBinding : TwoStatePreferenceBinding { + + override fun createWidget(context: Context): Preference = SwitchPreferenceCompat(context) companion object { @JvmStatic val INSTANCE = object : SwitchPreferenceBinding {} } } +/** A boolean value type preference associated with [MainSwitchPreference]. */ +interface MainSwitchPreferenceBinding : TwoStatePreferenceBinding { + + override fun createWidget(context: Context): Preference = MainSwitchPreference(context) + + companion object { + @JvmStatic val INSTANCE = object : MainSwitchPreferenceBinding {} + } +} + /** Default [PreferenceBinding] for [Preference]. */ object DefaultPreferenceBinding : PreferenceBinding diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp index 65b22758946d..00ae05ceabd4 100644 --- a/packages/SettingsProvider/Android.bp +++ b/packages/SettingsProvider/Android.bp @@ -76,6 +76,7 @@ android_test { "truth", "Nene", "Harrier", + "bedstead-enterprise", ], libs: [ "android.test.base.stubs.system", diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java index e86e72712b48..9cce43160b52 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java @@ -20,6 +20,9 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_ENABLED; import static android.provider.Settings.Secure.SYNC_PARENT_SOUNDS; import static android.provider.Settings.System.RINGTONE; +import static com.android.bedstead.enterprise.EnterpriseDeviceStateExtensionsKt.workProfile; +import static com.android.bedstead.multiuser.MultiUserDeviceStateExtensionsKt.secondaryUser; + import static com.google.common.truth.Truth.assertThat; import android.content.pm.PackageManager; @@ -82,7 +85,7 @@ public class SettingsProviderMultiUsersTest { @RequireFeature(PackageManager.FEATURE_MANAGED_USERS) @EnsureHasWorkProfile public void testSettings_workProfile() throws Exception { - UserReference profile = sDeviceState.workProfile(); + UserReference profile = workProfile(sDeviceState); // Settings.Global settings are shared between different users assertSettingsShared(SPACE_GLOBAL, mPrimaryUser.id(), profile.id()); @@ -96,7 +99,7 @@ public class SettingsProviderMultiUsersTest { @RequireRunOnInitialUser @EnsureHasSecondaryUser public void testSettings_secondaryUser() throws Exception { - UserReference secondaryUser = sDeviceState.secondaryUser(); + UserReference secondaryUser = secondaryUser(sDeviceState); // Settings.Global settings are shared between different users assertSettingsShared(SPACE_GLOBAL, mPrimaryUser.id(), secondaryUser.id()); @@ -223,7 +226,7 @@ public class SettingsProviderMultiUsersTest { @RequireRunOnInitialUser @EnsureHasSecondaryUser public void testSettings_stopAndRestartSecondaryUser() throws Exception { - UserReference secondaryUser = sDeviceState.secondaryUser(); + UserReference secondaryUser = secondaryUser(sDeviceState); assertSettingsDifferent(SPACE_SECURE, mPrimaryUser.id(), secondaryUser.id()); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt index 663c3418b144..16da3d22f4f7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt @@ -70,14 +70,14 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { } private fun createController() = - PrivacyDotViewController( + PrivacyDotViewControllerImpl( executor, testScope.backgroundScope, statusBarStateController, configurationController, contentInsetsProvider, animationScheduler = mock<SystemStatusAnimationScheduler>(), - shadeInteractor = null + shadeInteractor = null, ) .also { it.setUiExecutor(executor) } @@ -307,7 +307,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { newTopLeftView, newTopRightView, newBottomLeftView, - newBottomRightView + newBottomRightView, ) assertThat((newBottomRightView.layoutParams as FrameLayout.LayoutParams).gravity) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index dc9c22f566bf..f1edb417a314 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -21,6 +21,7 @@ import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE; import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -36,9 +37,11 @@ import static org.mockito.Mockito.when; import static java.util.Objects.requireNonNull; +import android.app.Flags; import android.database.ContentObserver; import android.os.Handler; import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import androidx.annotation.NonNull; @@ -61,6 +64,7 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustmentProvider; import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; +import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; @@ -69,6 +73,8 @@ import com.android.systemui.statusbar.notification.collection.provider.SectionSt import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.util.settings.SecureSettings; @@ -82,10 +88,12 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; @SmallTest @RunWith(AndroidJUnit4.class) @@ -93,6 +101,7 @@ import java.util.Map; public class PreparationCoordinatorTest extends SysuiTestCase { private NotifCollectionListener mCollectionListener; private OnBeforeFinalizeFilterListener mBeforeFilterListener; + private OnBeforeTransformGroupsListener mBeforeTransformGroupsListener; private NotifFilter mUninflatedFilter; private NotifFilter mInflationErrorFilter; private NotifInflationErrorManager mErrorManager; @@ -101,6 +110,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Captor private ArgumentCaptor<NotifCollectionListener> mCollectionListenerCaptor; @Captor private ArgumentCaptor<OnBeforeFinalizeFilterListener> mBeforeFilterListenerCaptor; + @Captor private ArgumentCaptor<OnBeforeTransformGroupsListener> + mBeforeTransformGroupsListenerCaptor; @Captor private ArgumentCaptor<NotifInflater.Params> mParamsCaptor; @Mock private NotifSectioner mNotifSectioner; @@ -108,13 +119,14 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Mock private NotifPipeline mNotifPipeline; @Mock private IStatusBarService mService; @Mock private BindEventManagerImpl mBindEventManagerImpl; + @Mock private AppIconProvider mAppIconProvider; + @Mock private NotificationIconStyleProvider mNotificationIconStyleProvider; @Mock private NotificationLockscreenUserManager mLockscreenUserManager; @Mock private SensitiveNotificationProtectionController mSensitiveNotifProtectionController; @Mock private Handler mHandler; @Mock private SecureSettings mSecureSettings; @Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater(); - @Mock - HighPriorityProvider mHighPriorityProvider; + @Mock HighPriorityProvider mHighPriorityProvider; private SectionStyleProvider mSectionStyleProvider; @Mock private UserTracker mUserTracker; @Mock private GroupMembershipManager mGroupMembershipManager; @@ -126,6 +138,11 @@ public class PreparationCoordinatorTest extends SysuiTestCase { return new NotificationEntryBuilder().setSection(mNotifSection); } + @NonNull + private GroupEntryBuilder getGroupEntryBuilder() { + return new GroupEntryBuilder().setSection(mNotifSection); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -138,7 +155,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mSectionStyleProvider, mUserTracker, mGroupMembershipManager - ); + ); mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build(); mInflationError = new Exception(TEST_MESSAGE); mErrorManager = new NotifInflationErrorManager(); @@ -153,6 +170,8 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mAdjustmentProvider, mService, mBindEventManagerImpl, + mAppIconProvider, + mNotificationIconStyleProvider, TEST_CHILD_BIND_CUTOFF, TEST_MAX_GROUP_DELAY); @@ -163,6 +182,15 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mInflationErrorFilter = filters.get(0); mUninflatedFilter = filters.get(1); + if (android.app.Flags.notificationsRedesignAppIcons()) { + verify(mNotifPipeline).addOnBeforeTransformGroupsListener( + mBeforeTransformGroupsListenerCaptor.capture()); + mBeforeTransformGroupsListener = mBeforeTransformGroupsListenerCaptor.getValue(); + } else { + verify(mNotifPipeline, never()).addOnBeforeTransformGroupsListener( + mBeforeTransformGroupsListenerCaptor.capture()); + } + verify(mNotifPipeline).addCollectionListener(mCollectionListenerCaptor.capture()); mCollectionListener = mCollectionListenerCaptor.getValue(); @@ -199,6 +227,100 @@ public class PreparationCoordinatorTest extends SysuiTestCase { } @Test + @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_APP_ICONS) + public void testPurgesAppIconProviderCache() { + // GIVEN a notification list + NotificationEntry entry1 = getNotificationEntryBuilder().setPkg("1").build(); + NotificationEntry entry2 = getNotificationEntryBuilder().setPkg("2").build(); + NotificationEntry entry2bis = getNotificationEntryBuilder().setPkg("2").build(); + NotificationEntry entry3 = getNotificationEntryBuilder().setPkg("3").build(); + + String groupKey1 = "group1"; + NotificationEntry summary = + getNotificationEntryBuilder() + .setPkg(groupKey1) + .setGroup(mContext, groupKey1) + .setGroupSummary(mContext, true) + .build(); + NotificationEntry child1 = getNotificationEntryBuilder().setGroup(mContext, groupKey1) + .setPkg(groupKey1).build(); + NotificationEntry child2 = getNotificationEntryBuilder().setGroup(mContext, groupKey1) + .setPkg(groupKey1).build(); + GroupEntry groupWithSummaryAndChildren = getGroupEntryBuilder().setKey(groupKey1) + .setSummary(summary).addChild(child1).addChild(child2).build(); + + String groupKey2 = "group2"; + NotificationEntry summary2 = + getNotificationEntryBuilder() + .setPkg(groupKey2) + .setGroup(mContext, groupKey2) + .setGroupSummary(mContext, true) + .build(); + GroupEntry summaryOnlyGroup = getGroupEntryBuilder().setKey(groupKey2) + .setSummary(summary2).build(); + + // WHEN onBeforeTransformGroup is called + mBeforeTransformGroupsListener.onBeforeTransformGroups( + List.of(entry1, entry2, entry2bis, entry3, + groupWithSummaryAndChildren, summaryOnlyGroup)); + + // THEN purge should be called + ArgumentCaptor<Collection<String>> argumentCaptor = ArgumentCaptor.forClass(List.class); + verify(mAppIconProvider).purgeCache(argumentCaptor.capture()); + List<String> actualList = argumentCaptor.getValue().stream().sorted().toList(); + List<String> expectedList = Stream.of("1", "2", "3", "group1", "group2") + .sorted().toList(); + assertEquals(expectedList, actualList); + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_APP_ICONS) + public void testPurgesNotificationIconStyleProviderCache() { + // GIVEN a notification list + NotificationEntry entry1 = getNotificationEntryBuilder().setPkg("1").build(); + NotificationEntry entry2 = getNotificationEntryBuilder().setPkg("2").build(); + NotificationEntry entry2bis = getNotificationEntryBuilder().setPkg("2").build(); + NotificationEntry entry3 = getNotificationEntryBuilder().setPkg("3").build(); + + String groupKey1 = "group1"; + NotificationEntry summary = + getNotificationEntryBuilder() + .setPkg(groupKey1) + .setGroup(mContext, groupKey1) + .setGroupSummary(mContext, true) + .build(); + NotificationEntry child1 = getNotificationEntryBuilder().setGroup(mContext, groupKey1) + .setPkg(groupKey1).build(); + NotificationEntry child2 = getNotificationEntryBuilder().setGroup(mContext, groupKey1) + .setPkg(groupKey1).build(); + GroupEntry groupWithSummaryAndChildren = getGroupEntryBuilder().setKey(groupKey1) + .setSummary(summary).addChild(child1).addChild(child2).build(); + + String groupKey2 = "group2"; + NotificationEntry summary2 = + getNotificationEntryBuilder() + .setPkg(groupKey2) + .setGroup(mContext, groupKey2) + .setGroupSummary(mContext, true) + .build(); + GroupEntry summaryOnlyGroup = getGroupEntryBuilder().setKey(groupKey2) + .setSummary(summary2).build(); + + // WHEN onBeforeTransformGroup is called + mBeforeTransformGroupsListener.onBeforeTransformGroups( + List.of(entry1, entry2, entry2bis, entry3, + groupWithSummaryAndChildren, summaryOnlyGroup)); + + // THEN purge should be called + ArgumentCaptor<Collection<String>> argumentCaptor = ArgumentCaptor.forClass(List.class); + verify(mNotificationIconStyleProvider).purgeCache(argumentCaptor.capture()); + List<String> actualList = argumentCaptor.getValue().stream().sorted().toList(); + List<String> expectedList = Stream.of("1", "2", "3", "group1", "group2") + .sorted().toList(); + assertEquals(expectedList, actualList); + } + + @Test public void testInflatesNewNotification() { // WHEN there is a new notification mCollectionListener.onEntryInit(mEntry); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt new file mode 100644 index 000000000000..7d5559933cd8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorTest.kt @@ -0,0 +1,99 @@ +/* + * 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.volume.dialog.ringer.domain + +import android.media.AudioManager.RINGER_MODE_NORMAL +import android.media.AudioManager.RINGER_MODE_SILENT +import android.media.AudioManager.RINGER_MODE_VIBRATE +import android.media.AudioManager.STREAM_RING +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.volume.shared.model.RingerMode +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.fakeVolumeDialogController +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +class VolumeDialogRingerInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val controller = kosmos.fakeVolumeDialogController + + private lateinit var underTest: VolumeDialogRingerInteractor + + @Before + fun setUp() { + underTest = kosmos.volumeDialogRingerInteractor + controller.setStreamVolume(STREAM_RING, 50) + } + + @Test + fun setRingerMode_normal() = + testScope.runTest { + runCurrent() + val ringerModel by collectLastValue(underTest.ringerModel) + + underTest.setRingerMode(RingerMode(RINGER_MODE_NORMAL)) + controller.getState() + runCurrent() + + assertThat(ringerModel).isNotNull() + assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_NORMAL)) + } + + @Test + fun setRingerMode_silent() = + testScope.runTest { + runCurrent() + val ringerModel by collectLastValue(underTest.ringerModel) + + underTest.setRingerMode(RingerMode(RINGER_MODE_SILENT)) + controller.getState() + runCurrent() + + assertThat(ringerModel).isNotNull() + assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_SILENT)) + } + + @Test + fun setRingerMode_vibrate() = + testScope.runTest { + runCurrent() + val ringerModel by collectLastValue(underTest.ringerModel) + + underTest.setRingerMode(RingerMode(RINGER_MODE_VIBRATE)) + controller.getState() + runCurrent() + + assertThat(ringerModel).isNotNull() + assertThat(ringerModel?.currentRingerMode).isEqualTo(RingerMode(RINGER_MODE_VIBRATE)) + } +} diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cdf15ca83dd9..c494e8525e0f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3745,6 +3745,10 @@ use. The helper shows shortcuts in categories, which can be collapsed or expanded. [CHAR LIMIT=NONE] --> <string name="shortcut_helper_content_description_drag_handle">Drag handle</string> + <!-- Label on the keyboard settings button in keyboard shortcut helper, that allows user to + open keyboard settings while in shortcut helper. The helper is a component that shows the + user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_keyboard_settings_buttons_label">Keyboard Settings</string> <!-- Keyboard touchpad tutorial scheduler--> diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 8ae11abab473..811b47d57c1d 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -43,7 +43,7 @@ import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dump.DumpManager; import com.android.systemui.process.ProcessWrapper; import com.android.systemui.res.R; -import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.phone.ConfigurationForwarder; import com.android.systemui.util.NotificationChannels; import java.lang.reflect.InvocationTargetException; @@ -454,13 +454,13 @@ public class SystemUIApplication extends Application implements @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { if (mServicesStarted) { - ConfigurationController configController = mSysUIComponent.getConfigurationController(); + ConfigurationForwarder configForwarder = mSysUIComponent.getConfigurationForwarder(); if (Trace.isEnabled()) { Trace.traceBegin( Trace.TRACE_TAG_APP, - configController.getClass().getSimpleName() + ".onConfigurationChanged()"); + configForwarder.getClass().getSimpleName() + ".onConfigurationChanged()"); } - configController.onConfigurationChanged(newConfig); + configForwarder.onConfigurationChanged(newConfig); Trace.endSection(); } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 3fe6669de556..17f1961e662c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -29,6 +29,7 @@ import com.android.systemui.people.PeopleProvider; import com.android.systemui.startable.Dependencies; import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; +import com.android.systemui.statusbar.phone.ConfigurationForwarder; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; @@ -125,13 +126,20 @@ public interface SysUIComponent { BootCompleteCacheImpl provideBootCacheImpl(); /** - * Creates a ContextComponentHelper. + * Creates a ConfigurationController. */ @SysUISingleton @GlobalConfig ConfigurationController getConfigurationController(); /** + * Creates a ConfigurationForwarder. + */ + @SysUISingleton + @GlobalConfig + ConfigurationForwarder getConfigurationForwarder(); + + /** * Creates a ContextComponentHelper. */ @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index 21922ff22afe..12718e8bd119 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -17,6 +17,7 @@ package com.android.systemui.doze; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; +import static android.hardware.biometrics.Flags.screenOffUnlockUdfps; import static com.android.systemui.doze.DozeLog.REASON_SENSOR_QUICK_PICKUP; import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS; @@ -248,8 +249,8 @@ public class DozeSensors { true /* touchscreen */, false /* ignoresSetting */, dozeParameters.longPressUsesProx(), - false /* immediatelyReRegister */, - true /* requiresAod */ + screenOffUnlockUdfps() /* immediatelyReRegister */, + !screenOffUnlockUdfps() /* requiresAod */ ), new PluginSensor( new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY), diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index 901eafa29418..5cade686ae09 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -777,7 +777,7 @@ private fun KeyboardSettings(horizontalPadding: Dp, verticalPadding: Dp, onClick ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( - "Keyboard Settings", + stringResource(id = R.string.shortcut_helper_keyboard_settings_buttons_label), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 16.sp, ) 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 6db91ac073ba..4071b135dfaf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -221,7 +221,7 @@ constructor( { notificationScrimClippingParams.params.top }, // Only allow scrolling when we are fully expanded. That way, we don't intercept // swipes in lockscreen (when somehow QS is receiving touches). - { scrollState.canScrollForward && viewModel.isQsFullyExpanded }, + { (scrollState.canScrollForward && viewModel.isQsFullyExpanded) || isCustomizing }, ) frame.addView( composeView, 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 71fa0ac30fb7..7b2593952599 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 @@ -77,25 +77,24 @@ fun LargeTileContent( colors: TileColors, squishiness: () -> Float, accessibilityUiState: AccessibilityUiState? = null, - toggleClickSupported: Boolean = false, iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), - onClick: () -> Unit = {}, - onLongClick: () -> Unit = {}, + toggleClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = tileHorizontalArrangement(), ) { // Icon - val longPressLabel = longPressLabel() + val longPressLabel = longPressLabel().takeIf { onLongClick != null } Box( modifier = - Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClickSupported) { + Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) { Modifier.clip(iconShape) .verticalSquish(squishiness) .background(colors.iconBackground, { 1f }) .combinedClickable( - onClick = onClick, + onClick = toggleClick!!, onLongClick = onLongClick, onLongClickLabel = longPressLabel, ) 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 d2ec958c17b7..b581c8bf953f 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 @@ -202,14 +202,17 @@ fun DefaultEditTileGrid( topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) }, ) { innerPadding -> CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + val scrollState = rememberScrollState() + LaunchedEffect(listState.dragInProgress) { + if (listState.dragInProgress) { + scrollState.animateScrollTo(0) + } + } + Column( verticalArrangement = spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), - modifier = - modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(innerPadding), + modifier = modifier.fillMaxSize().verticalScroll(scrollState).padding(innerPadding), ) { AnimatedContent( targetState = listState.dragInProgress, 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 52d526123430..5f28fe427707 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 @@ -160,19 +160,18 @@ fun Tile( ) } else { val iconShape = TileDefaults.animateIconShape(uiState.state) + val secondaryClick: (() -> Unit)? = + { tile.onSecondaryClick() }.takeIf { uiState.handlesSecondaryClick } + val longClick: (() -> Unit)? = + { tile.onLongClick(expandable) }.takeIf { uiState.handlesLongClick } LargeTileContent( label = uiState.label, secondaryLabel = uiState.secondaryLabel, icon = icon, colors = colors, iconShape = iconShape, - toggleClickSupported = state.handlesSecondaryClick, - onClick = { - if (state.handlesSecondaryClick) { - tile.onSecondaryClick() - } - }, - onLongClick = { tile.onLongClick(expandable) }, + toggleClick = secondaryClick, + onLongClick = longClick, accessibilityUiState = uiState.accessibilityUiState, squishiness = squishiness, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt index aa420800be7b..56675e49d4e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt @@ -33,6 +33,7 @@ data class TileUiState( val label: String, val secondaryLabel: String, val state: Int, + val handlesLongClick: Boolean, val handlesSecondaryClick: Boolean, val icon: Supplier<QSTile.Icon?>, val accessibilityUiState: AccessibilityUiState, @@ -86,6 +87,7 @@ fun QSTile.State.toUiState(resources: Resources): TileUiState { label = label?.toString() ?: "", secondaryLabel = secondaryLabel?.toString() ?: "", state = if (disabledByPolicy) Tile.STATE_UNAVAILABLE else state, + handlesLongClick = handlesLongClick, handlesSecondaryClick = handlesSecondaryClick, icon = icon?.let { Supplier { icon } } ?: iconSupplier ?: Supplier { null }, AccessibilityUiState( diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.kt new file mode 100644 index 000000000000..111d335d0d0f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAware.kt @@ -0,0 +1,33 @@ +/* + * 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.shade + +import javax.inject.Qualifier + +/** + * Qualifies classes that provide display-specific info for shade window components. + * + * The Shade window can be moved between displays with different characteristics (e.g., density, + * size). This annotation ensures that components within the shade window use the correct context + * and resources for the display they are currently on. + * + * Classes annotated with `@ShadeDisplayAware` (e.g., 'Context`, `Resources`, `LayoutInflater`, + * `ConfigurationController`) will be dynamically updated to reflect the current display's + * configuration. This ensures consistent rendering even when the shade window is moved to an + * external display. + */ +@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class ShadeDisplayAware diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index 2930de2fd9ee..0eef8d63c2d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -44,8 +44,12 @@ import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN import com.android.systemui.util.leak.RotationUtils.Rotation +import dagger.Module +import dagger.Provides +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import java.util.concurrent.Executor -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -63,26 +67,57 @@ import kotlinx.coroutines.launch * NOTE: any operation that modifies views directly must run on the provided executor, because these * views are owned by ScreenDecorations and it runs in its own thread */ -@SysUISingleton -open class PrivacyDotViewController -@Inject +interface PrivacyDotViewController { + + // Only can be modified on @UiThread + var currentViewState: ViewState + + var showingListener: ShowingListener? + + fun setUiExecutor(e: DelayableExecutor) + + fun getUiExecutor(): DelayableExecutor? + + @UiThread fun setNewRotation(rot: Int) + + @UiThread fun hideDotView(dot: View, animate: Boolean) + + @UiThread fun showDotView(dot: View, animate: Boolean) + + // Update the gravity and margins of the privacy views + @UiThread fun updateRotations(rotation: Int, paddingTop: Int) + + @UiThread fun setCornerSizes(state: ViewState) + + fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) + + @UiThread fun updateDotView(state: ViewState) + + interface ShowingListener { + fun onPrivacyDotShown(v: View?) + + fun onPrivacyDotHidden(v: View?) + } +} + +open class PrivacyDotViewControllerImpl +@AssistedInject constructor( @Main private val mainExecutor: Executor, - @Application scope: CoroutineScope, + @Assisted scope: CoroutineScope, private val stateController: StatusBarStateController, - private val configurationController: ConfigurationController, - private val contentInsetsProvider: StatusBarContentInsetsProvider, + @Assisted private val configurationController: ConfigurationController, + @Assisted private val contentInsetsProvider: StatusBarContentInsetsProvider, private val animationScheduler: SystemStatusAnimationScheduler, - shadeInteractor: ShadeInteractor? -) { + shadeInteractor: ShadeInteractor?, +) : PrivacyDotViewController { private lateinit var tl: View private lateinit var tr: View private lateinit var bl: View private lateinit var br: View // Only can be modified on @UiThread - var currentViewState: ViewState = ViewState() - get() = field + override var currentViewState: ViewState = ViewState() @GuardedBy("lock") private var nextViewState: ViewState = currentViewState.copy() @@ -100,11 +135,7 @@ constructor( private val views: Sequence<View> get() = if (!this::tl.isInitialized) sequenceOf() else sequenceOf(tl, tr, br, bl) - var showingListener: ShowingListener? = null - set(value) { - field = value - } - get() = field + override var showingListener: PrivacyDotViewController.ShowingListener? = null init { contentInsetsProvider.addCallback( @@ -153,16 +184,16 @@ constructor( } } - fun setUiExecutor(e: DelayableExecutor) { + override fun setUiExecutor(e: DelayableExecutor) { uiExecutor = e } - fun getUiExecutor(): DelayableExecutor? { + override fun getUiExecutor(): DelayableExecutor? { return uiExecutor } @UiThread - fun setNewRotation(rot: Int) { + override fun setNewRotation(rot: Int) { dlog("updateRotation: $rot") val isRtl: Boolean @@ -187,13 +218,13 @@ constructor( rotation = rot, paddingTop = paddingTop, designatedCorner = newCorner, - cornerIndex = index + cornerIndex = index, ) } } @UiThread - fun hideDotView(dot: View, animate: Boolean) { + override fun hideDotView(dot: View, animate: Boolean) { dot.clearAnimation() if (animate) { dot.animate() @@ -212,7 +243,7 @@ constructor( } @UiThread - fun showDotView(dot: View, animate: Boolean) { + override fun showDotView(dot: View, animate: Boolean) { dot.clearAnimation() if (animate) { dot.visibility = View.VISIBLE @@ -229,9 +260,8 @@ constructor( showingListener?.onPrivacyDotShown(dot) } - // Update the gravity and margins of the privacy views @UiThread - open fun updateRotations(rotation: Int, paddingTop: Int) { + override fun updateRotations(rotation: Int, paddingTop: Int) { // To keep a view in the corner, its gravity is always the description of its current corner // Therefore, just figure out which view is in which corner. This turns out to be something // like (myCorner - rot) mod 4, where topLeft = 0, topRight = 1, etc. and portrait = 0, and @@ -262,7 +292,7 @@ constructor( } @UiThread - open fun setCornerSizes(state: ViewState) { + override fun setCornerSizes(state: ViewState) { // StatusBarContentInsetsProvider can tell us the location of the privacy indicator dot // in every rotation. The only thing we need to check is rtl val rtl = state.layoutRtl @@ -415,7 +445,7 @@ constructor( } } - fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) { + override fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) { if ( this::tl.isInitialized && this::tr.isInitialized && @@ -457,7 +487,7 @@ constructor( landscapeRect = right, upsideDownRect = bottom, paddingTop = paddingTop, - layoutRtl = rtl + layoutRtl = rtl, ) } } @@ -533,7 +563,7 @@ constructor( } @UiThread - open fun updateDotView(state: ViewState) { + override fun updateDotView(state: ViewState) { val shouldShow = state.shouldShowDot() if (shouldShow != currentViewState.shouldShowDot()) { if (shouldShow && state.designatedCorner != null) { @@ -553,7 +583,7 @@ constructor( nextViewState = nextViewState.copy( systemPrivacyEventIsActive = true, - contentDescription = contentDescr + contentDescription = contentDescr, ) } @@ -595,15 +625,18 @@ constructor( seascapeRect = rects[0], portraitRect = rects[1], landscapeRect = rects[2], - upsideDownRect = rects[3] + upsideDownRect = rects[3], ) } } - interface ShowingListener { - fun onPrivacyDotShown(v: View?) - - fun onPrivacyDotHidden(v: View?) + @AssistedFactory + interface Factory { + fun create( + scope: CoroutineScope, + configurationController: ConfigurationController, + contentInsetsProvider: StatusBarContentInsetsProvider, + ): PrivacyDotViewControllerImpl } } @@ -662,7 +695,7 @@ data class ViewState( val paddingTop: Int = 0, val cornerIndex: Int = -1, val designatedCorner: View? = null, - val contentDescription: String? = null + val contentDescription: String? = null, ) { fun shouldShowDot(): Boolean { return systemPrivacyEventIsActive && !shadeExpanded && !qsExpanded @@ -687,3 +720,18 @@ data class ViewState( } } } + +@Module +object PrivacyDotViewControllerModule { + + @Provides + @SysUISingleton + fun controller( + factory: PrivacyDotViewControllerImpl.Factory, + @Application scope: CoroutineScope, + configurationController: ConfigurationController, + contentInsetsProvider: StatusBarContentInsetsProvider, + ): PrivacyDotViewController { + return factory.create(scope, configurationController, contentInsetsProvider) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt index 231a0b0b21cb..9fe4a5499ceb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt @@ -32,13 +32,13 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow interface NotificationActivityStarter { /** Called when the user clicks on the notification bubble icon. */ - fun onNotificationBubbleIconClicked(entry: NotificationEntry?) + fun onNotificationBubbleIconClicked(entry: NotificationEntry) /** Called when the user clicks on the surface of a notification. */ - fun onNotificationClicked(entry: NotificationEntry?, row: ExpandableNotificationRow?) + fun onNotificationClicked(entry: NotificationEntry, row: ExpandableNotificationRow) /** Called when the user clicks on a button in the notification guts which fires an intent. */ - fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?) + fun startNotificationGutsIntent(intent: Intent, appUid: Int, row: ExpandableNotificationRow) /** * Called when the user clicks "Manage" or "History" in the Shade. Prefer using @@ -56,7 +56,7 @@ interface NotificationActivityStarter { fun startSettingsIntent(view: View, intentInfo: SettingsIntent) /** Called when the user succeed to drop notification to proper target view. */ - fun onDragSuccess(entry: NotificationEntry?) + fun onDragSuccess(entry: NotificationEntry) val isCollapsingToShowActivityOverLockscreen: Boolean get() = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt index 2ee1dffd14f3..958001625a07 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt @@ -35,6 +35,9 @@ import java.util.concurrent.atomic.AtomicInteger * This cache is safe for multithreaded usage, and is recommended for objects that take a while to * resolve (such as drawables, or things that require binder calls). As such, [getOrFetch] is * recommended to be run on a background thread, while [purge] can be done from any thread. + * + * Important: This cache does NOT have a maximum size, cleaning it up (via [purge]) is the + * responsibility of the caller, to avoid keeping things in memory unnecessarily. */ @SuppressLint("DumpableNotRegistered") // this will be dumped by container classes class NotifCollectionCache<V>( @@ -151,7 +154,7 @@ class NotifCollectionCache<V>( * purge((c)); // deletes a from the cache and marks b for deletion, etc. * ``` */ - fun purge(wantedKeys: List<String>) { + fun purge(wantedKeys: Collection<String>) { for ((key, entry) in cache) { if (key in wantedKeys) { entry.resetLives() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 9b075a650b48..f75163d2662b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -27,6 +27,7 @@ import android.os.Trace; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -49,13 +50,18 @@ import com.android.systemui.statusbar.notification.collection.render.NotifViewBa import com.android.systemui.statusbar.notification.collection.render.NotifViewController; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager.NotifInflationErrorListener; +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.inject.Inject; @@ -104,6 +110,8 @@ public class PreparationCoordinator implements Coordinator { /** How long we can delay a group while waiting for all children to inflate */ private final long mMaxGroupInflationDelay; private final BindEventManagerImpl mBindEventManager; + private final AppIconProvider mAppIconProvider; + private final NotificationIconStyleProvider mNotificationIconStyleProvider; @Inject public PreparationCoordinator( @@ -113,7 +121,9 @@ public class PreparationCoordinator implements Coordinator { NotifViewBarn viewBarn, NotifUiAdjustmentProvider adjustmentProvider, IStatusBarService service, - BindEventManagerImpl bindEventManager) { + BindEventManagerImpl bindEventManager, + AppIconProvider appIconProvider, + NotificationIconStyleProvider notificationIconStyleProvider) { this( logger, notifInflater, @@ -122,6 +132,8 @@ public class PreparationCoordinator implements Coordinator { adjustmentProvider, service, bindEventManager, + appIconProvider, + notificationIconStyleProvider, CHILD_BIND_CUTOFF, MAX_GROUP_INFLATION_DELAY); } @@ -135,6 +147,8 @@ public class PreparationCoordinator implements Coordinator { NotifUiAdjustmentProvider adjustmentProvider, IStatusBarService service, BindEventManagerImpl bindEventManager, + AppIconProvider appIconProvider, + NotificationIconStyleProvider notificationIconStyleProvider, int childBindCutoff, long maxGroupInflationDelay) { mLogger = logger; @@ -146,6 +160,8 @@ public class PreparationCoordinator implements Coordinator { mChildBindCutoff = childBindCutoff; mMaxGroupInflationDelay = maxGroupInflationDelay; mBindEventManager = bindEventManager; + mAppIconProvider = appIconProvider; + mNotificationIconStyleProvider = notificationIconStyleProvider; } @Override @@ -155,6 +171,9 @@ public class PreparationCoordinator implements Coordinator { () -> mNotifInflatingFilter.invalidateList("adjustmentProviderChanged")); pipeline.addCollectionListener(mNotifCollectionListener); + if (android.app.Flags.notificationsRedesignAppIcons()) { + pipeline.addOnBeforeTransformGroupsListener(this::purgeCaches); + } // Inflate after grouping/sorting since that affects what views to inflate. pipeline.addOnBeforeFinalizeFilterListener(this::inflateAllRequiredViews); pipeline.addFinalizeFilter(mNotifInflationErrorFilter); @@ -260,6 +279,29 @@ public class PreparationCoordinator implements Coordinator { } }; + private void purgeCaches(Collection<ListEntry> entries) { + Set<String> wantedPackages = getPackages(entries); + mAppIconProvider.purgeCache(wantedPackages); + mNotificationIconStyleProvider.purgeCache(wantedPackages); + } + + /** + * Get all app packages present in {@param entries}. + */ + private static @NonNull Set<String> getPackages(Collection<ListEntry> entries) { + Set<String> packages = new HashSet<>(); + for (ListEntry entry : entries) { + NotificationEntry notificationEntry = entry.getRepresentativeEntry(); + if (notificationEntry == null) { + Log.wtf(TAG, "notification entry " + entry.getKey() + + " has no representative entry"); + continue; + } + packages.add(notificationEntry.getSbn().getPackageName()); + } + return packages; + } + private void inflateAllRequiredViews(List<ListEntry> entries) { for (int i = 0, size = entries.size(); i < size; i++) { ListEntry entry = entries.get(i); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt index 24b5cf1aa33b..0ddf9f7270df 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row.icon +import android.annotation.WorkerThread import android.app.ActivityManager import android.app.Flags import android.content.Context @@ -27,20 +28,45 @@ import android.graphics.drawable.Drawable import android.util.Log import com.android.internal.R import com.android.launcher3.icons.BaseIconFactory +import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.collection.NotifCollectionCache +import com.android.systemui.util.asIndenting +import com.android.systemui.util.withIncreasedIndent import dagger.Module import dagger.Provides +import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider /** A provider used to cache and fetch app icons used by notifications. */ interface AppIconProvider { + /** + * Loads the icon corresponding to [packageName] into cache, or fetches it from there if already + * present. This should only be called from the background. + */ @Throws(NameNotFoundException::class) + @WorkerThread fun getOrFetchAppIcon(packageName: String, context: Context): Drawable + + /** + * Mark all the entries in the cache that are NOT in [wantedPackages] to be cleared. If they're + * still not needed on the next call of this method (made after a timeout of 1s, in case they + * happen more frequently than that), they will be purged. This can be done from any thread. + */ + fun purgeCache(wantedPackages: Collection<String>) } @SysUISingleton -class AppIconProviderImpl @Inject constructor(private val sysuiContext: Context) : AppIconProvider { +class AppIconProviderImpl +@Inject +constructor(private val sysuiContext: Context, dumpManager: DumpManager) : + AppIconProvider, Dumpable { + init { + dumpManager.registerNormalDumpable(TAG, this) + } + private val iconFactory: BaseIconFactory get() { val isLowRam = ActivityManager.isLowRamDeviceStatic() @@ -53,13 +79,42 @@ class AppIconProviderImpl @Inject constructor(private val sysuiContext: Context) return BaseIconFactory(sysuiContext, res.configuration.densityDpi, iconSize) } + private val cache = NotifCollectionCache<Drawable>() + override fun getOrFetchAppIcon(packageName: String, context: Context): Drawable { + return cache.getOrFetch(packageName) { fetchAppIcon(packageName, context) } + } + + @WorkerThread + private fun fetchAppIcon(packageName: String, context: Context): BitmapDrawable { val icon = context.packageManager.getApplicationIcon(packageName) return BitmapDrawable( context.resources, iconFactory.createScaledBitmap(icon, BaseIconFactory.MODE_HARDWARE), ) } + + override fun purgeCache(wantedPackages: Collection<String>) { + cache.purge(wantedPackages) + } + + override fun dump(pwOrig: PrintWriter, args: Array<out String>) { + val pw = pwOrig.asIndenting() + + pw.println("cache information:") + pw.withIncreasedIndent { cache.dump(pw, args) } + + val iconFactory = iconFactory + pw.println("icon factory information:") + pw.withIncreasedIndent { + pw.println("fullResIconDpi = ${iconFactory.fullResIconDpi}") + pw.println("iconSize = ${iconFactory.iconBitmapSize}") + } + } + + companion object { + const val TAG = "AppIconProviderImpl" + } } class NoOpIconProvider : AppIconProvider { @@ -71,6 +126,10 @@ class NoOpIconProvider : AppIconProvider { Log.wtf(TAG, "NoOpIconProvider should not be used anywhere.") return ColorDrawable(Color.WHITE) } + + override fun purgeCache(wantedPackages: Collection<String>) { + Log.wtf(TAG, "NoOpIconProvider should not be used anywhere.") + } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProvider.kt index 165c1a7803a9..35e38c2c0c14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProvider.kt @@ -22,9 +22,15 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.service.notification.StatusBarNotification import android.util.Log +import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.collection.NotifCollectionCache +import com.android.systemui.util.asIndenting +import com.android.systemui.util.withIncreasedIndent import dagger.Module import dagger.Provides +import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider @@ -33,15 +39,35 @@ import javax.inject.Provider * notifications. */ interface NotificationIconStyleProvider { + /** + * Determines whether the [notification] should display the app icon instead of the small icon. + * This can result in a binder call, and therefore should only be called from the background. + */ @WorkerThread fun shouldShowAppIcon(notification: StatusBarNotification, context: Context): Boolean + + /** + * Mark all the entries in the cache that are NOT in [wantedPackages] to be cleared. If they're + * still not needed on the next call of this method (made after a timeout of 1s, in case they + * happen more frequently than that), they will be purged. This can be done from any thread. + */ + fun purgeCache(wantedPackages: Collection<String>) } @SysUISingleton -class NotificationIconStyleProviderImpl @Inject constructor() : NotificationIconStyleProvider { +class NotificationIconStyleProviderImpl @Inject constructor(dumpManager: DumpManager) : + NotificationIconStyleProvider, Dumpable { + init { + dumpManager.registerNormalDumpable(TAG, this) + } + + private val cache = NotifCollectionCache<Boolean>() + override fun shouldShowAppIcon(notification: StatusBarNotification, context: Context): Boolean { val packageContext = notification.getPackageContext(context) - return !belongsToHeadlessSystemApp(packageContext) + return cache.getOrFetch(notification.packageName) { + !belongsToHeadlessSystemApp(packageContext) + } } @WorkerThread @@ -62,6 +88,20 @@ class NotificationIconStyleProviderImpl @Inject constructor() : NotificationIcon return false } } + + override fun purgeCache(wantedPackages: Collection<String>) { + cache.purge(wantedPackages) + } + + override fun dump(pwOrig: PrintWriter, args: Array<out String>) { + val pw = pwOrig.asIndenting() + pw.println("cache information:") + pw.withIncreasedIndent { cache.dump(pw, args) } + } + + companion object { + const val TAG = "NotificationIconStyleProviderImpl" + } } class NoOpIconStyleProvider : NotificationIconStyleProvider { @@ -73,6 +113,10 @@ class NoOpIconStyleProvider : NotificationIconStyleProvider { Log.wtf(TAG, "NoOpIconStyleProvider should not be used anywhere.") return true } + + override fun purgeCache(wantedPackages: Collection<String>) { + Log.wtf(TAG, "NoOpIconStyleProvider should not be used anywhere.") + } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt index a8c823c35213..858cac111525 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt @@ -29,9 +29,8 @@ import dagger.assisted.AssistedInject class ConfigurationControllerImpl @AssistedInject -constructor( - @Assisted private val context: Context, -) : ConfigurationController, StatusBarConfigurationController { +constructor(@Assisted private val context: Context) : + ConfigurationController, StatusBarConfigurationController { private val listeners: MutableList<ConfigurationListener> = ArrayList() private val lastConfig = Configuration() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt new file mode 100644 index 000000000000..3fd46fc484a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import android.content.res.Configuration + +/** + * Used to forward a configuration change to other components. + * + * This is commonly used to propagate configs to [ConfigurationController]. Note that there could be + * different configuration forwarder, for example each display, window or group of classes (e.g. + * shade window classes). + */ +interface ConfigurationForwarder { + /** Should be called when a new configuration is received. */ + fun onConfigurationChanged(newConfiguration: Configuration) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 93db2db918b0..af98311c937f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -20,7 +20,6 @@ import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED import static android.service.notification.NotificationListenerService.REASON_CLICK; import static com.android.systemui.statusbar.phone.CentralSurfaces.getActivityOptions; -import static com.android.systemui.util.kotlin.NullabilityKt.expectNotNull; import android.app.ActivityManager; import android.app.ActivityOptions; @@ -231,8 +230,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit * @param entry notification that bubble icon was clicked */ @Override - public void onNotificationBubbleIconClicked(NotificationEntry entry) { - expectNotNull(TAG, "entry", entry); + public void onNotificationBubbleIconClicked(@NonNull NotificationEntry entry) { Runnable action = () -> { mBubblesManagerOptional.ifPresent(bubblesManager -> bubblesManager.onUserChangedBubble(entry, !entry.isBubble())); @@ -258,9 +256,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit * @param row row for that notification */ @Override - public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) { - expectNotNull(TAG, "entry", entry); - expectNotNull(TAG, "row", row); + public void onNotificationClicked(@NonNull NotificationEntry entry, + @NonNull ExpandableNotificationRow row) { mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(), mKeyguardStateController.isVisible(), mNotificationShadeWindowController.getPanelExpanded()); @@ -442,8 +439,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit * @param entry notification entry that is dropped. */ @Override - public void onDragSuccess(NotificationEntry entry) { - expectNotNull(TAG, "entry", entry); + public void onDragSuccess(@NonNull NotificationEntry entry) { // this method is not responsible for intent sending. // will focus follow operation only after drag-and-drop that notification. final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); @@ -534,10 +530,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } @Override - public void startNotificationGutsIntent(final Intent intent, final int appUid, - ExpandableNotificationRow row) { - expectNotNull(TAG, "intent", intent); - expectNotNull(TAG, "row", row); + public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid, + @NonNull ExpandableNotificationRow row) { boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */); ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt index 09e191dd1911..92d0ebecf8d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.core.StatusBarInitializerStore import com.android.systemui.statusbar.core.StatusBarOrchestrator import com.android.systemui.statusbar.core.StatusBarSimpleFragment import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore +import com.android.systemui.statusbar.events.PrivacyDotViewControllerModule import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks import com.android.systemui.statusbar.window.StatusBarWindowControllerStore import com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore @@ -45,7 +46,7 @@ import dagger.multibindings.IntoMap import kotlinx.coroutines.CoroutineScope /** Similar in purpose to [StatusBarModule], but scoped only to phones */ -@Module +@Module(includes = [PrivacyDotViewControllerModule::class]) interface StatusBarPhoneModule { @Binds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java index cec77c12a40b..1bb4e8c66ef1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java @@ -16,16 +16,15 @@ package com.android.systemui.statusbar.policy; import android.content.res.Configuration; +import com.android.systemui.statusbar.phone.ConfigurationForwarder; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; /** * Common listener for configuration or subsets of configuration changes (like density or * font scaling), providing easy static dependence on these events. */ -public interface ConfigurationController extends CallbackController<ConfigurationListener> { - - /** Alert controller of a change in the configuration. */ - void onConfigurationChanged(Configuration newConfiguration); +public interface ConfigurationController extends CallbackController<ConfigurationListener>, + ConfigurationForwarder { /** Alert controller of a change in between light and dark themes. */ void notifyThemeChanged(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index b81af86b0779..c7bd5a1bb9a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.NetworkControllerImpl; import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory; import com.android.systemui.statusbar.phone.ConfigurationControllerImpl; +import com.android.systemui.statusbar.phone.ConfigurationForwarder; import com.android.systemui.statusbar.policy.BatteryControllerLogger; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.BluetoothControllerImpl; @@ -186,6 +187,13 @@ public interface StatusBarPolicyModule { DevicePostureControllerImpl devicePostureControllerImpl); /** */ + @Binds + @SysUISingleton + @GlobalConfig + ConfigurationForwarder provideGlobalConfigurationForwarder( + @GlobalConfig ConfigurationController configurationController); + + /** */ @Provides @SysUISingleton @GlobalConfig diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index e89a31f8fad3..618722a79a6f 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -27,7 +27,10 @@ import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenCon import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureRecognizer +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @Composable fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { @@ -48,7 +51,17 @@ fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni ), ) val recognizer = rememberBackGestureRecognizer(LocalContext.current.resources) - GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack) + val gestureUiState: Flow<GestureUiState> = + remember(recognizer) { + GestureFlowAdapter(recognizer).gestureStateAsFlow.map { + it.toGestureUiState( + progressStartMark = "", + progressEndMark = "", + successAnimation = R.raw.trackpad_back_success, + ) + } + } + GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack) } @Composable diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index 7899f5b42a25..11e1ff49074a 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt @@ -17,6 +17,7 @@ package com.android.systemui.touchpad.tutorial.ui.composable import androidx.activity.compose.BackHandler +import androidx.annotation.RawRes import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box @@ -31,23 +32,49 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.inputdevice.tutorial.ui.composable.ActionTutorialContent import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig +import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished +import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.NotStarted import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress -import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler +import kotlinx.coroutines.flow.Flow -fun GestureState.toTutorialActionState(): TutorialActionState { +sealed interface GestureUiState { + data object NotStarted : GestureUiState + + data class Finished(@RawRes val successAnimation: Int) : GestureUiState + + data class InProgress( + val progress: Float = 0f, + val progressStartMark: String = "", + val progressEndMark: String = "", + ) : GestureUiState +} + +fun GestureState.toGestureUiState( + progressStartMark: String, + progressEndMark: String, + successAnimation: Int, +): GestureUiState { + return when (this) { + GestureState.NotStarted -> NotStarted + is GestureState.InProgress -> + GestureUiState.InProgress(this.progress, progressStartMark, progressEndMark) + is GestureState.Finished -> GestureUiState.Finished(successAnimation) + } +} + +fun GestureUiState.toTutorialActionState(): TutorialActionState { return when (this) { NotStarted -> TutorialActionState.NotStarted // progress is disabled for now as views are not ready to handle varying progress - is InProgress -> TutorialActionState.InProgress(0f) - Finished -> TutorialActionState.Finished + is GestureUiState.InProgress -> TutorialActionState.InProgress(progress = 0f) + is Finished -> TutorialActionState.Finished } } @@ -55,15 +82,13 @@ fun GestureState.toTutorialActionState(): TutorialActionState { fun GestureTutorialScreen( screenConfig: TutorialScreenConfig, gestureRecognizer: GestureRecognizer, + gestureUiStateFlow: Flow<GestureUiState>, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { BackHandler(onBack = onBack) - var gestureState: GestureState by remember { mutableStateOf(NotStarted) } var easterEggTriggered by remember { mutableStateOf(false) } - LaunchedEffect(gestureRecognizer) { - gestureRecognizer.addGestureStateCallback { gestureState = it } - } + val gestureState by gestureUiStateFlow.collectAsStateWithLifecycle(NotStarted) val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered = true } val gestureHandler = remember(gestureRecognizer) { TouchpadGestureHandler(gestureRecognizer, easterEggMonitor) } @@ -84,7 +109,7 @@ fun GestureTutorialScreen( @Composable private fun TouchpadGesturesHandlingBox( gestureHandler: TouchpadGestureHandler, - gestureState: GestureState, + gestureState: GestureUiState, easterEggTriggered: Boolean, resetEasterEggFlag: () -> Unit, modifier: Modifier = Modifier, @@ -110,7 +135,7 @@ private fun TouchpadGesturesHandlingBox( .pointerInteropFilter( onTouchEvent = { event -> // FINISHED is the final state so we don't need to process touches anymore - if (gestureState == Finished) { + if (gestureState is Finished) { false } else { gestureHandler.onMotionEvent(event) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index 3ddf760b9704..05871ce91e39 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -25,8 +25,11 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @Composable fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { @@ -47,7 +50,17 @@ fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni ), ) val recognizer = rememberHomeGestureRecognizer(LocalContext.current.resources) - GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack) + val gestureUiState: Flow<GestureUiState> = + remember(recognizer) { + GestureFlowAdapter(recognizer).gestureStateAsFlow.map { + it.toGestureUiState( + progressStartMark = "", + progressEndMark = "", + successAnimation = R.raw.trackpad_home_success, + ) + } + } + GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack) } @Composable diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt index 30a21bf3b632..4fd16448e9f2 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt @@ -25,8 +25,11 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @Composable fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { @@ -47,7 +50,17 @@ fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () ), ) val recognizer = rememberRecentAppsGestureRecognizer(LocalContext.current.resources) - GestureTutorialScreen(screenConfig, recognizer, onDoneButtonClicked, onBack) + val gestureUiState: Flow<GestureUiState> = + remember(recognizer) { + GestureFlowAdapter(recognizer).gestureStateAsFlow.map { + it.toGestureUiState( + progressStartMark = "", + progressEndMark = "", + successAnimation = R.raw.trackpad_recent_apps_success, + ) + } + } + GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack) } @Composable diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt index 80f800390852..024048cbbb24 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt @@ -33,6 +33,10 @@ class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu gestureStateChangedCallback = callback } + override fun clearGestureStateCallback() { + gestureStateChangedCallback = {} + } + override fun accept(event: MotionEvent) { if (!isThreeFingerTouchpadSwipe(event)) return val gestureState = distanceTracker.processEvent(event) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt new file mode 100644 index 000000000000..23e31b0a9efd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureFlowAdapter.kt @@ -0,0 +1,30 @@ +/* + * 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.touchpad.tutorial.ui.gesture + +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +class GestureFlowAdapter(gestureRecognizer: GestureRecognizer) { + + val gestureStateAsFlow: Flow<GestureState> = conflatedCallbackFlow { + val callback: (GestureState) -> Unit = { trySend(it) } + gestureRecognizer.addGestureStateCallback(callback) + awaitClose { gestureRecognizer.clearGestureStateCallback() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt index d146268304a6..68a2ef9528eb 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureRecognizer.kt @@ -22,6 +22,8 @@ import java.util.function.Consumer /** Based on passed [MotionEvent]s recognizes different states of gesture and notifies callback. */ interface GestureRecognizer : Consumer<MotionEvent> { fun addGestureStateCallback(callback: (GestureState) -> Unit) + + fun clearGestureStateCallback() } fun isThreeFingerTouchpadSwipe(event: MotionEvent) = isNFingerTouchpadSwipe(event, fingerCount = 3) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt index 2b84a4c50613..b804b9a0d4c7 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt @@ -29,6 +29,10 @@ class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu gestureStateChangedCallback = callback } + override fun clearGestureStateCallback() { + gestureStateChangedCallback = {} + } + override fun accept(event: MotionEvent) { if (!isThreeFingerTouchpadSwipe(event)) return val gestureState = distanceTracker.processEvent(event) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt index 69b7c5edd750..7d484ee85b7c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt @@ -38,6 +38,10 @@ class RecentAppsGestureRecognizer( gestureStateChangedCallback = callback } + override fun clearGestureStateCallback() { + gestureStateChangedCallback = {} + } + override fun accept(event: MotionEvent) { if (!isThreeFingerTouchpadSwipe(event)) return val gestureState = distanceTracker.processEvent(event) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepository.kt deleted file mode 100644 index 73b97f642ec9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepository.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.volume.dialog.ringer.data - -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.update - -/** Stores the state of volume dialog ringer model */ -@SysUISingleton -class VolumeDialogRingerRepository @Inject constructor() { - - private val mutableRingerModel = MutableStateFlow<VolumeDialogRingerModel?>(null) - val ringerModel: Flow<VolumeDialogRingerModel> = mutableRingerModel.filterNotNull() - - fun updateRingerModel(update: (current: VolumeDialogRingerModel?) -> VolumeDialogRingerModel) { - mutableRingerModel.update(update) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt new file mode 100644 index 000000000000..7265b82148ba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt @@ -0,0 +1,84 @@ +/* + * 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.volume.dialog.ringer.domain + +import android.media.AudioManager +import android.media.AudioManager.RINGER_MODE_NORMAL +import android.media.AudioManager.RINGER_MODE_SILENT +import android.media.AudioManager.RINGER_MODE_VIBRATE +import android.provider.Settings +import com.android.settingslib.volume.shared.model.RingerMode +import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog +import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor +import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel +import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn + +/** Exposes [VolumeDialogRingerModel]. */ +@VolumeDialog +class VolumeDialogRingerInteractor +@Inject +constructor( + @VolumeDialog private val coroutineScope: CoroutineScope, + volumeDialogStateInteractor: VolumeDialogStateInteractor, + private val controller: VolumeDialogController, +) { + + val ringerModel: Flow<VolumeDialogRingerModel> = + volumeDialogStateInteractor.volumeDialogState + .mapNotNull { toRingerModel(it) } + .stateIn(coroutineScope, SharingStarted.Eagerly, null) + .filterNotNull() + + private fun toRingerModel(state: VolumeDialogStateModel): VolumeDialogRingerModel? { + return state.streamModels[AudioManager.STREAM_RING]?.let { + VolumeDialogRingerModel( + availableModes = + mutableListOf(RingerMode(RINGER_MODE_NORMAL), RingerMode(RINGER_MODE_SILENT)) + .also { list -> + if (controller.hasVibrator()) { + list.add(RingerMode(RINGER_MODE_VIBRATE)) + } + }, + currentRingerMode = RingerMode(state.ringerModeInternal), + isEnabled = + !(state.zenMode == Settings.Global.ZEN_MODE_ALARMS || + state.zenMode == Settings.Global.ZEN_MODE_NO_INTERRUPTIONS || + (state.zenMode == Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS && + state.disallowRinger)), + isMuted = it.level == 0 || it.muted, + level = it.level, + levelMax = it.levelMax, + ) + } + } + + fun setRingerMode(ringerMode: RingerMode) { + controller.setRingerMode(ringerMode.value, false) + } + + fun scheduleTouchFeedback() { + controller.scheduleTouchFeedback() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java index 3d1a0d0cef3c..96f4a60271d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java @@ -481,6 +481,25 @@ public class DozeTriggersTest extends SysuiTestCase { verify(mAuthController).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat()); } + @Test + @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS) + public void udfpsLongPress_triggeredWhenDoze() { + // GIVEN device is DOZE + when(mMachine.getState()).thenReturn(DozeMachine.State.DOZE); + + // WHEN udfps long-press is triggered + mTriggers.onSensor(DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS, 100, 100, + new float[]{0, 1, 2, 3, 4}); + + // THEN the pulse is NOT dropped + verify(mDozeLog, never()).tracePulseDropped(anyString(), any()); + + // WHEN the screen state is OFF + mTriggers.onScreenState(Display.STATE_OFF); + + // THEN aod interrupt never be sent + verify(mAuthController, never()).onAodInterrupt(anyInt(), anyInt(), anyFloat(), anyFloat()); + } @Test public void udfpsLongPress_dozeState_notRegistered() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index 0b5f8d5e948c..723c0d701305 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -74,22 +74,31 @@ import com.android.systemui.testKosmos import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.wmshell.BubblesManager import java.util.Optional -import junit.framework.Assert import kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runCurrent +import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doNothing +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever /** Tests for [NotificationGutsManager] with the scene container enabled. */ +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -99,7 +108,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { NotificationChannel( TEST_CHANNEL_ID, TEST_CHANNEL_ID, - NotificationManager.IMPORTANCE_DEFAULT + NotificationManager.IMPORTANCE_DEFAULT, ) private val kosmos = testKosmos() @@ -146,7 +155,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) allowTestableLooperAsMainThread() helper = NotificationTestHelper(mContext, mDependency) - Mockito.`when`(accessibilityManager.isTouchExplorationEnabled).thenReturn(false) + whenever(accessibilityManager.isTouchExplorationEnabled).thenReturn(false) windowRootViewVisibilityInteractor = WindowRootViewVisibilityInteractor( testScope.backgroundScope, @@ -185,12 +194,12 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { deviceProvisionedController, metricsLogger, headsUpManager, - activityStarter + activityStarter, ) gutsManager.setUpWithPresenter( presenter, notificationListContainer, - onSettingsClickListener + onSettingsClickListener, ) gutsManager.setNotificationActivityStarter(notificationActivityStarter) gutsManager.start() @@ -198,49 +207,31 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { @Test fun testOpenAndCloseGuts() { - val guts = Mockito.spy(NotificationGuts(mContext)) - Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock - -> + val guts = spy(NotificationGuts(mContext)) + whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock -> handler.post((invocation.arguments[0] as Runnable)) null } // Test doesn't support animation since the guts view is not attached. - Mockito.doNothing() - .`when`(guts) - .openControls( - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.any(Runnable::class.java) - ) + doNothing() + .whenever(guts) + .openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>()) val realRow = createTestNotificationRow() val menuItem = createTestMenuItem(realRow) - val row = Mockito.spy(realRow) - Mockito.`when`(row!!.windowToken).thenReturn(Binder()) - Mockito.`when`(row.guts).thenReturn(guts) + val row = spy(realRow) + whenever(row!!.windowToken).thenReturn(Binder()) + whenever(row.guts).thenReturn(guts) Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) assertEquals(View.INVISIBLE.toLong(), guts.visibility.toLong()) executor.runAllReady() - verify(guts) - .openControls( - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.any(Runnable::class.java) - ) + verify(guts).openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>()) verify(headsUpManager).setGutsShown(realRow!!.entry, true) assertEquals(View.VISIBLE.toLong(), guts.visibility.toLong()) gutsManager.closeAndSaveGuts(false, false, true, 0, 0, false) verify(guts) - .closeControls( - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyBoolean() - ) - verify(row, Mockito.times(1)).setGutsView(ArgumentMatchers.any()) + .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>()) + verify(row, times(1)).setGutsView(any()) executor.runAllReady() verify(headsUpManager).setGutsShown(realRow.entry, false) } @@ -250,7 +241,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { // First, start out lockscreen or shade as not visible setIsLockscreenOrShadeVisible(false) testScope.testScheduler.runCurrent() - val guts = Mockito.mock(NotificationGuts::class.java) + val guts = mock<NotificationGuts>() gutsManager.exposedGuts = guts // WHEN the lockscreen or shade becomes visible @@ -258,15 +249,9 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { testScope.testScheduler.runCurrent() // THEN the guts are not closed - verify(guts, Mockito.never()).removeCallbacks(ArgumentMatchers.any()) - verify(guts, Mockito.never()) - .closeControls( - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyBoolean() - ) + verify(guts, never()).removeCallbacks(any()) + verify(guts, never()) + .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>()) } @Test @@ -274,7 +259,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { // First, start out lockscreen or shade as visible setIsLockscreenOrShadeVisible(true) testScope.testScheduler.runCurrent() - val guts = Mockito.mock(NotificationGuts::class.java) + val guts = mock<NotificationGuts>() gutsManager.exposedGuts = guts // WHEN the lockscreen or shade is no longer visible @@ -282,14 +267,14 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { testScope.testScheduler.runCurrent() // THEN the guts are closed - verify(guts).removeCallbacks(ArgumentMatchers.any()) + verify(guts).removeCallbacks(anyOrNull()) verify(guts) .closeControls( - /* leavebehinds= */ ArgumentMatchers.eq(true), - /* controls= */ ArgumentMatchers.eq(true), - /* x= */ ArgumentMatchers.anyInt(), - /* y= */ ArgumentMatchers.anyInt(), - /* force= */ ArgumentMatchers.eq(true) + /* leavebehinds= */ eq(true), + /* controls= */ eq(true), + /* x= */ any<Int>(), + /* y= */ any<Int>(), + /* force= */ eq(true), ) } @@ -304,95 +289,68 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { testScope.testScheduler.runCurrent() // THEN the list container is reset - verify(notificationListContainer) - .resetExposedMenuView(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyBoolean()) + verify(notificationListContainer).resetExposedMenuView(any<Boolean>(), any<Boolean>()) } @Test fun testChangeDensityOrFontScale() { - val guts = Mockito.spy(NotificationGuts(mContext)) - Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock - -> + val guts = spy(NotificationGuts(mContext)) + whenever(guts.post(any())).thenAnswer { invocation: InvocationOnMock -> handler.post((invocation.arguments[0] as Runnable)) null } // Test doesn't support animation since the guts view is not attached. - Mockito.doNothing() - .`when`(guts) - .openControls( - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.any(Runnable::class.java) - ) + doNothing() + .whenever(guts) + .openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>()) val realRow = createTestNotificationRow() val menuItem = createTestMenuItem(realRow) - val row = Mockito.spy(realRow) - Mockito.`when`(row!!.windowToken).thenReturn(Binder()) - Mockito.`when`(row.guts).thenReturn(guts) - Mockito.doNothing().`when`(row).ensureGutsInflated() + val row = spy(realRow) + whenever(row!!.windowToken).thenReturn(Binder()) + whenever(row.guts).thenReturn(guts) + doNothing().whenever(row).ensureGutsInflated() val realEntry = realRow!!.entry - val entry = Mockito.spy(realEntry) - Mockito.`when`(entry.row).thenReturn(row) - Mockito.`when`(entry.getGuts()).thenReturn(guts) + val entry = spy(realEntry) + whenever(entry.row).thenReturn(row) + whenever(entry.getGuts()).thenReturn(guts) Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) executor.runAllReady() - verify(guts) - .openControls( - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.any(Runnable::class.java) - ) + verify(guts).openControls(any<Int>(), any<Int>(), any<Boolean>(), any<Runnable>()) // called once by mGutsManager.bindGuts() in mGutsManager.openGuts() - verify(row).setGutsView(ArgumentMatchers.any()) + verify(row).setGutsView(any()) row.onDensityOrFontScaleChanged() gutsManager.onDensityOrFontScaleChanged(entry) executor.runAllReady() gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false) verify(guts) - .closeControls( - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.anyBoolean(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.anyBoolean() - ) + .closeControls(any<Boolean>(), any<Boolean>(), any<Int>(), any<Int>(), any<Boolean>()) // called again by mGutsManager.bindGuts(), in mGutsManager.onDensityOrFontScaleChanged() - verify(row, Mockito.times(2)).setGutsView(ArgumentMatchers.any()) + verify(row, times(2)).setGutsView(any()) } @Test fun testAppOpsSettingsIntent_camera() { val ops = ArraySet<Int>() ops.add(AppOpsManager.OP_CAMERA) - gutsManager.startAppOpsSettingsActivity("", 0, ops, null) - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(notificationActivityStarter, Mockito.times(1)) - .startNotificationGutsIntent( - captor.capture(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action) + gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>()) + val captor = argumentCaptor<Intent>() + verify(notificationActivityStarter, times(1)) + .startNotificationGutsIntent(captor.capture(), any<Int>(), any()) + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action) } @Test fun testAppOpsSettingsIntent_mic() { val ops = ArraySet<Int>() ops.add(AppOpsManager.OP_RECORD_AUDIO) - gutsManager.startAppOpsSettingsActivity("", 0, ops, null) - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(notificationActivityStarter, Mockito.times(1)) - .startNotificationGutsIntent( - captor.capture(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action) + gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>()) + val captor = argumentCaptor<Intent>() + verify(notificationActivityStarter, times(1)) + .startNotificationGutsIntent(captor.capture(), any<Int>(), any()) + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action) } @Test @@ -400,30 +358,22 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { val ops = ArraySet<Int>() ops.add(AppOpsManager.OP_CAMERA) ops.add(AppOpsManager.OP_RECORD_AUDIO) - gutsManager.startAppOpsSettingsActivity("", 0, ops, null) - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(notificationActivityStarter, Mockito.times(1)) - .startNotificationGutsIntent( - captor.capture(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action) + gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>()) + val captor = argumentCaptor<Intent>() + verify(notificationActivityStarter, times(1)) + .startNotificationGutsIntent(captor.capture(), any<Int>(), any()) + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.lastValue.action) } @Test fun testAppOpsSettingsIntent_overlay() { val ops = ArraySet<Int>() ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) - gutsManager.startAppOpsSettingsActivity("", 0, ops, null) - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(notificationActivityStarter, Mockito.times(1)) - .startNotificationGutsIntent( - captor.capture(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - assertEquals(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION, captor.value.action) + gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>()) + val captor = argumentCaptor<Intent>() + verify(notificationActivityStarter, times(1)) + .startNotificationGutsIntent(captor.capture(), any<Int>(), any()) + assertEquals(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION, captor.lastValue.action) } @Test @@ -432,15 +382,11 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { ops.add(AppOpsManager.OP_CAMERA) ops.add(AppOpsManager.OP_RECORD_AUDIO) ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) - gutsManager.startAppOpsSettingsActivity("", 0, ops, null) - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(notificationActivityStarter, Mockito.times(1)) - .startNotificationGutsIntent( - captor.capture(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action) + gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>()) + val captor = argumentCaptor<Intent>() + verify(notificationActivityStarter, times(1)) + .startNotificationGutsIntent(captor.capture(), any<Int>(), any()) + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action) } @Test @@ -448,15 +394,11 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { val ops = ArraySet<Int>() ops.add(AppOpsManager.OP_CAMERA) ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) - gutsManager.startAppOpsSettingsActivity("", 0, ops, null) - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(notificationActivityStarter, Mockito.times(1)) - .startNotificationGutsIntent( - captor.capture(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action) + gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>()) + val captor = argumentCaptor<Intent>() + verify(notificationActivityStarter, times(1)) + .startNotificationGutsIntent(captor.capture(), any<Int>(), any()) + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action) } @Test @@ -464,112 +406,108 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { val ops = ArraySet<Int>() ops.add(AppOpsManager.OP_RECORD_AUDIO) ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) - gutsManager.startAppOpsSettingsActivity("", 0, ops, null) - val captor = ArgumentCaptor.forClass(Intent::class.java) - verify(notificationActivityStarter, Mockito.times(1)) - .startNotificationGutsIntent( - captor.capture(), - ArgumentMatchers.anyInt(), - ArgumentMatchers.any() - ) - assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action) + gutsManager.startAppOpsSettingsActivity("", 0, ops, mock<ExpandableNotificationRow>()) + val captor = argumentCaptor<Intent>() + verify(notificationActivityStarter, times(1)) + .startNotificationGutsIntent(captor.capture(), any<Int>(), any()) + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.lastValue.action) } @Test @Throws(Exception::class) fun testInitializeNotificationInfoView_highPriority() { - val notificationInfoView = Mockito.mock(NotificationInfo::class.java) - val row = Mockito.spy(helper.createRow()) + val notificationInfoView = mock<NotificationInfo>() + val row = spy(helper.createRow()) val entry = row.entry NotificationEntryHelper.modifyRanking(entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .setImportance(NotificationManager.IMPORTANCE_HIGH) .build() - Mockito.`when`(row.getIsNonblockable()).thenReturn(false) - Mockito.`when`(highPriorityProvider.isHighPriority(entry)).thenReturn(true) + whenever(row.getIsNonblockable()).thenReturn(false) + whenever(highPriorityProvider.isHighPriority(entry)).thenReturn(true) val statusBarNotification = entry.sbn gutsManager.initializeNotificationInfo(row, notificationInfoView) verify(notificationInfoView) .bindNotification( - ArgumentMatchers.any(PackageManager::class.java), - ArgumentMatchers.any(INotificationManager::class.java), - ArgumentMatchers.eq(onUserInteractionCallback), - ArgumentMatchers.eq(channelEditorDialogController), - ArgumentMatchers.eq(statusBarNotification.packageName), - ArgumentMatchers.any(NotificationChannel::class.java), - ArgumentMatchers.eq(entry), - ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java), - ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java), - ArgumentMatchers.any(UiEventLogger::class.java), - ArgumentMatchers.eq(true), - ArgumentMatchers.eq(false), - ArgumentMatchers.eq(true), /* wasShownHighPriority */ - ArgumentMatchers.eq(assistantFeedbackController), - ArgumentMatchers.any(MetricsLogger::class.java) + any<PackageManager>(), + any<INotificationManager>(), + eq(onUserInteractionCallback), + eq(channelEditorDialogController), + eq(statusBarNotification.packageName), + any<NotificationChannel>(), + eq(entry), + any<NotificationInfo.OnSettingsClickListener>(), + any<NotificationInfo.OnAppSettingsClickListener>(), + any<UiEventLogger>(), + eq(true), + eq(false), + eq(true), /* wasShownHighPriority */ + eq(assistantFeedbackController), + any<MetricsLogger>(), ) } @Test @Throws(Exception::class) fun testInitializeNotificationInfoView_PassesAlongProvisionedState() { - val notificationInfoView = Mockito.mock(NotificationInfo::class.java) - val row = Mockito.spy(helper.createRow()) + val notificationInfoView = mock<NotificationInfo>() + val row = spy(helper.createRow()) NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .build() - Mockito.`when`(row.getIsNonblockable()).thenReturn(false) + whenever(row.getIsNonblockable()).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry gutsManager.initializeNotificationInfo(row, notificationInfoView) verify(notificationInfoView) .bindNotification( - ArgumentMatchers.any(PackageManager::class.java), - ArgumentMatchers.any(INotificationManager::class.java), - ArgumentMatchers.eq(onUserInteractionCallback), - ArgumentMatchers.eq(channelEditorDialogController), - ArgumentMatchers.eq(statusBarNotification.packageName), - ArgumentMatchers.any(NotificationChannel::class.java), - ArgumentMatchers.eq(entry), - ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java), - ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java), - ArgumentMatchers.any(UiEventLogger::class.java), - ArgumentMatchers.eq(true), - ArgumentMatchers.eq(false), - ArgumentMatchers.eq(false), /* wasShownHighPriority */ - ArgumentMatchers.eq(assistantFeedbackController), - ArgumentMatchers.any(MetricsLogger::class.java) + any<PackageManager>(), + any<INotificationManager>(), + eq(onUserInteractionCallback), + eq(channelEditorDialogController), + eq(statusBarNotification.packageName), + any<NotificationChannel>(), + eq(entry), + any<NotificationInfo.OnSettingsClickListener>(), + any<NotificationInfo.OnAppSettingsClickListener>(), + any<UiEventLogger>(), + eq(true), + eq(false), + eq(false), /* wasShownHighPriority */ + eq(assistantFeedbackController), + any<MetricsLogger>(), ) } @Test @Throws(Exception::class) fun testInitializeNotificationInfoView_withInitialAction() { - val notificationInfoView = Mockito.mock(NotificationInfo::class.java) - val row = Mockito.spy(helper.createRow()) + val notificationInfoView = mock<NotificationInfo>() + val row = spy(helper.createRow()) NotificationEntryHelper.modifyRanking(row.entry) .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) .build() - Mockito.`when`(row.getIsNonblockable()).thenReturn(false) + whenever(row.getIsNonblockable()).thenReturn(false) val statusBarNotification = row.entry.sbn val entry = row.entry gutsManager.initializeNotificationInfo(row, notificationInfoView) verify(notificationInfoView) .bindNotification( - ArgumentMatchers.any(PackageManager::class.java), - ArgumentMatchers.any(INotificationManager::class.java), - ArgumentMatchers.eq(onUserInteractionCallback), - ArgumentMatchers.eq(channelEditorDialogController), - ArgumentMatchers.eq(statusBarNotification.packageName), - ArgumentMatchers.any(NotificationChannel::class.java), - ArgumentMatchers.eq(entry), - ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java), - ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java), - ArgumentMatchers.any(UiEventLogger::class.java), - ArgumentMatchers.eq(true), - ArgumentMatchers.eq(false), - ArgumentMatchers.eq(false), /* wasShownHighPriority */ - ArgumentMatchers.eq(assistantFeedbackController), - ArgumentMatchers.any(MetricsLogger::class.java) + any<PackageManager>(), + any<INotificationManager>(), + eq(onUserInteractionCallback), + eq(channelEditorDialogController), + eq(statusBarNotification.packageName), + any<NotificationChannel>(), + eq(entry), + any<NotificationInfo.OnSettingsClickListener>(), + any<NotificationInfo.OnAppSettingsClickListener>(), + any<UiEventLogger>(), + eq(true), + eq(false), + eq(false), /* wasShownHighPriority */ + eq(assistantFeedbackController), + any<MetricsLogger>(), ) } 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 7f4c670b05aa..c3996e400726 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 @@ -85,7 +85,6 @@ import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent import com.android.systemui.util.Assert.runWithCurrentThreadAsMainThread import com.android.systemui.util.DeviceConfigProxyFake import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.wmshell.BubblesManager @@ -126,6 +125,7 @@ class ExpandableNotificationRowBuilder( private val mMainCoroutineContext = mTestScope.coroutineContext private val mFakeSystemClock = FakeSystemClock() private val mMainExecutor = FakeExecutor(mFakeSystemClock) + private val mDumpManager = DumpManager() init { featureFlags.setDefault(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE) @@ -142,8 +142,7 @@ class ExpandableNotificationRowBuilder( mGroupMembershipManager = GroupMembershipManagerImpl() mSmartReplyController = Mockito.mock(SmartReplyController::class.java, STUB_ONLY) - val dumpManager = DumpManager() - mGroupExpansionManager = GroupExpansionManagerImpl(dumpManager, mGroupMembershipManager) + mGroupExpansionManager = GroupExpansionManagerImpl(mDumpManager, mGroupMembershipManager) mHeadsUpManager = Mockito.mock(HeadsUpManager::class.java, STUB_ONLY) mIconManager = IconManager( @@ -289,8 +288,8 @@ class ExpandableNotificationRowBuilder( NotificationOptimizedLinearLayoutFactory(), { Mockito.mock(NotificationViewFlipperFactory::class.java) }, NotificationRowIconViewInflaterFactory( - AppIconProviderImpl(context), - NotificationIconStyleProviderImpl(), + AppIconProviderImpl(context, mDumpManager), + NotificationIconStyleProviderImpl(mDumpManager), ), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt index 08c6bbab6dd6..0fd0f1469818 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/AppIconProviderKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.row.icon import android.content.applicationContext +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos -val Kosmos.appIconProvider by Kosmos.Fixture { AppIconProviderImpl(applicationContext) } +val Kosmos.appIconProvider by + Kosmos.Fixture { AppIconProviderImpl(applicationContext, dumpManager) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt index 611c90a6f4e8..0fe84fb135ab 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/icon/NotificationIconStyleProviderKosmos.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row.icon +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos -val Kosmos.notificationIconStyleProvider by Kosmos.Fixture { NotificationIconStyleProviderImpl() } +val Kosmos.notificationIconStyleProvider by + Kosmos.Fixture { NotificationIconStyleProviderImpl(dumpManager) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt index 6be13be407d8..32191277c94a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt @@ -23,7 +23,7 @@ class FakeConfigurationController @Inject constructor() : listeners -= listener } - override fun onConfigurationChanged(newConfiguration: Configuration?) { + override fun onConfigurationChanged(newConfiguration: Configuration) { listeners.forEach { it.onConfigChanged(newConfiguration) } } @@ -36,7 +36,7 @@ class FakeConfigurationController @Inject constructor() : } fun notifyConfigurationChanged() { - onConfigurationChanged(newConfiguration = null) + onConfigurationChanged(newConfiguration = Configuration()) } fun notifyLayoutDirectionChanged(isRtl: Boolean) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt index bca13c6f502d..65247a55348d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt @@ -24,6 +24,6 @@ class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory { override fun create( context: Context, viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, - statusBarConfigurationController: StatusBarConfigurationController + statusBarConfigurationController: StatusBarConfigurationController, ) = FakeStatusBarWindowController() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt index 2c518863cf3c..c2a1544820c5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/VolumeDialogRingerRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt @@ -14,8 +14,18 @@ * limitations under the License. */ -package com.android.systemui.volume.dialog.ringer.data +package com.android.systemui.volume.dialog.ringer.domain import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor -val Kosmos.volumeDialogRingerRepository by Kosmos.Fixture { VolumeDialogRingerRepository() } +val Kosmos.volumeDialogRingerInteractor by + Kosmos.Fixture { + VolumeDialogRingerInteractor( + coroutineScope = applicationCoroutineScope, + volumeDialogStateInteractor = volumeDialogStateInteractor, + controller = volumeDialogController, + ) + } diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 39ac5150c7f1..363807d2aa8c 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -68,6 +68,7 @@ import android.telephony.CellSignalStrengthWcdma; import android.telephony.DisconnectCause; import android.telephony.LinkCapacityEstimate; import android.telephony.LocationAccessPolicy; +import android.telephony.NetworkRegistrationInfo; import android.telephony.PhoneCapability; import android.telephony.PhoneStateListener; import android.telephony.PhysicalChannelConfig; @@ -90,6 +91,7 @@ import android.telephony.ims.MediaQualityStatus; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IntArray; import android.util.LocalLog; import android.util.Pair; import android.util.SparseArray; @@ -429,6 +431,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private boolean[] mCarrierRoamingNtnMode = null; private boolean[] mCarrierRoamingNtnEligible = null; + private List<IntArray> mCarrierRoamingNtnAvailableServices; + /** * Per-phone map of precise data connection state. The key of the map is the pair of transport * type and APN setting. This is the cache to prevent redundant callbacks to the listeners. @@ -741,6 +745,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { cutListToSize(mCarrierServiceStates, mNumPhones); cutListToSize(mCallStateLists, mNumPhones); cutListToSize(mMediaQualityStatus, mNumPhones); + cutListToSize(mCarrierRoamingNtnAvailableServices, mNumPhones); return; } @@ -789,6 +794,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mSCBMDuration[i] = 0; mCarrierRoamingNtnMode[i] = false; mCarrierRoamingNtnEligible[i] = false; + mCarrierRoamingNtnAvailableServices.add(i, new IntArray()); } } } @@ -864,6 +870,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mSCBMDuration = new long[numPhones]; mCarrierRoamingNtnMode = new boolean[numPhones]; mCarrierRoamingNtnEligible = new boolean[numPhones]; + mCarrierRoamingNtnAvailableServices = new ArrayList<>(); for (int i = 0; i < numPhones; i++) { mCallState[i] = TelephonyManager.CALL_STATE_IDLE; @@ -909,6 +916,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mSCBMDuration[i] = 0; mCarrierRoamingNtnMode[i] = false; mCarrierRoamingNtnEligible[i] = false; + mCarrierRoamingNtnAvailableServices.add(i, new IntArray()); } mAppOps = mContext.getSystemService(AppOpsManager.class); @@ -1533,6 +1541,15 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { remove(r.binder); } } + if (events.contains( + TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED)) { + try { + r.callback.onCarrierRoamingNtnAvailableServicesChanged( + mCarrierRoamingNtnAvailableServices.get(r.phoneId).toArray()); + } catch (RemoteException ex) { + remove(r.binder); + } + } } } } @@ -3642,6 +3659,47 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } + /** + * Notify external listeners that carrier roaming non-terrestrial available services changed. + * @param availableServices The list of the supported services. + */ + public void notifyCarrierRoamingNtnAvailableServicesChanged( + int subId, @NetworkRegistrationInfo.ServiceType int[] availableServices) { + if (!checkNotifyPermission("notifyCarrierRoamingNtnEligibleStateChanged")) { + log("notifyCarrierRoamingNtnAvailableServicesChanged: caller does not have required " + + "permissions."); + return; + } + + if (VDBG) { + log("notifyCarrierRoamingNtnAvailableServicesChanged: " + + "availableServices=" + Arrays.toString(availableServices)); + } + + synchronized (mRecords) { + int phoneId = getPhoneIdFromSubId(subId); + if (!validatePhoneId(phoneId)) { + loge("Invalid phone ID " + phoneId + " for " + subId); + return; + } + IntArray availableServicesIntArray = new IntArray(availableServices.length); + availableServicesIntArray.addAll(availableServices); + mCarrierRoamingNtnAvailableServices.set(phoneId, availableServicesIntArray); + for (Record r : mRecords) { + if (r.matchTelephonyCallbackEvent( + TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_AVAILABLE_SERVICES_CHANGED) + && idMatch(r, subId, phoneId)) { + try { + r.callback.onCarrierRoamingNtnAvailableServicesChanged(availableServices); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } + } + } + handleRemoveListLocked(); + } + } + @NeverCompile // Avoid size overhead of debugging code. @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { @@ -3706,6 +3764,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { Pair<String, Integer> carrierServiceState = mCarrierServiceStates.get(i); pw.println("mCarrierServiceState=<package=" + pii(carrierServiceState.first) + ", uid=" + carrierServiceState.second + ">"); + pw.println("mCarrierRoamingNtnAvailableServices=" + + mCarrierRoamingNtnAvailableServices.get(i)); pw.decreaseIndent(); } diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java index ba4b71cd7540..17fcbf47206f 100644 --- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java @@ -22,7 +22,7 @@ import static com.android.server.Watchdog.NATIVE_STACKS_OF_INTEREST; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ANR; import static com.android.server.am.ActivityManagerService.MY_PID; import static com.android.server.am.ProcessRecord.TAG; -import static com.android.server.stats.pull.ProcfsMemoryUtil.readMemorySnapshotFromProcfs; +import static com.android.internal.os.ProcfsMemoryUtil.readMemorySnapshotFromProcfs; import android.annotation.Nullable; import android.app.ActivityManager; @@ -58,7 +58,7 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.modules.expresslog.Counter; import com.android.server.ResourcePressureUtil; import com.android.server.criticalevents.CriticalEventLog; -import com.android.server.stats.pull.ProcfsMemoryUtil.MemorySnapshot; +import com.android.internal.os.ProcfsMemoryUtil.MemorySnapshot; import com.android.server.wm.WindowProcessController; import java.io.File; diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java index 1d68ee54d96c..83b0801ce87f 100644 --- a/services/core/java/com/android/server/display/BrightnessRangeController.java +++ b/services/core/java/com/android/server/display/BrightnessRangeController.java @@ -67,6 +67,10 @@ class BrightnessRangeController { mNormalBrightnessModeController.resetNbmData( displayDeviceConfig.getLuxThrottlingData()); } + if (flags.useNewHdrBrightnessModifier()) { + // HDR boost is handled by HdrBrightnessModifier and should be disabled in HbmController + mHbmController.disableHdrBoost(); + } updateHdrClamper(info, displayToken, displayDeviceConfig); } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 42a62f098b6a..5b61f006def6 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1503,7 +1503,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // use the current brightness setting scaled by the doze scale factor rawBrightnessState = getDozeBrightnessForOffload(); brightnessState = clampScreenBrightness(rawBrightnessState); - updateScreenBrightnessSetting = false; mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_MANUAL); mTempBrightnessEvent.setFlags( mTempBrightnessEvent.getFlags() | BrightnessEvent.FLAG_DOZE_SCALE); @@ -1513,6 +1512,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call brightnessState = clampScreenBrightness(rawBrightnessState); mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT); } + updateScreenBrightnessSetting = false; } if (!mFlags.isRefactorDisplayPowerControllerEnabled()) { diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java index 135cab6d0614..6be0c123d262 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeController.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java @@ -38,6 +38,7 @@ import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.util.FrameworkStatsLog; import com.android.server.display.DisplayManagerService.Clock; import com.android.server.display.config.HighBrightnessModeData; +import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.utils.DebugUtils; import java.io.PrintWriter; @@ -119,6 +120,14 @@ class HighBrightnessModeController { @Nullable private HighBrightnessModeMetadata mHighBrightnessModeMetadata; + /** + * If {@link DisplayManagerFlags#useNewHdrBrightnessModifier()} is ON, hdr boost is handled by + * {@link com.android.server.display.brightness.clamper.HdrBrightnessModifier} and should be + * disabled in this class. After flag is cleaned up, this field together with HDR handling + * should be cleaned up from this class. + */ + private boolean mHdrBoostDisabled = false; + HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken, String displayUniqueId, float brightnessMin, float brightnessMax, HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg, @@ -323,6 +332,7 @@ class HighBrightnessModeController { pw.println(" mIsTimeAvailable= " + mIsTimeAvailable); pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode); pw.println(" width*height=" + mWidth + "*" + mHeight); + pw.println(" mHdrBoostDisabled=" + mHdrBoostDisabled); if (mHighBrightnessModeMetadata != null) { pw.println(" mRunningStartTimeMillis=" @@ -373,6 +383,11 @@ class HighBrightnessModeController { return mHbmData != null && mHighBrightnessModeMetadata != null; } + void disableHdrBoost() { + mHdrBoostDisabled = true; + unregisterHdrListener(); + } + private long calculateRemainingTime(long currentTime) { if (!deviceSupportsHbm()) { return 0; @@ -583,6 +598,9 @@ class HighBrightnessModeController { } private void registerHdrListener(IBinder displayToken) { + if (mHdrBoostDisabled) { + return; + } if (mRegisteredDisplayToken == displayToken) { return; } diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index b488db533d12..2f5236f51c48 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -23,6 +23,7 @@ import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; +import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut; import android.annotation.BinderThread; import android.annotation.MainThread; @@ -654,6 +655,18 @@ final class KeyGestureController { } } break; + case KeyEvent.KEYCODE_D: + if (enableMoveToNextDisplayShortcut()) { + if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) { + return handleKeyGesture(deviceId, new int[]{keyCode}, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + displayId, + focusedToken, /* flags = */0); + } + } + break; case KeyEvent.KEYCODE_SLASH: if (firstDown && event.isMetaPressed()) { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java index d1576c5cca4f..509fa3e1c9ba 100644 --- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java +++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java @@ -127,42 +127,18 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { @BinderThread public void updateRuleSet( String version, ParceledListSlice<Rule> rules, IntentSender statusReceiver) { - String ruleProvider = getCallerPackageNameOrThrow(Binder.getCallingUid()); - if (DEBUG_INTEGRITY_COMPONENT) { - Slog.i(TAG, String.format("Calling rule provider name is: %s.", ruleProvider)); + Intent intent = new Intent(); + intent.putExtra(EXTRA_STATUS, STATUS_SUCCESS); + try { + statusReceiver.sendIntent( + mContext, + /* code= */ 0, + intent, + /* onFinished= */ null, + /* handler= */ null); + } catch (Exception e) { + Slog.e(TAG, "Error sending status feedback.", e); } - - mHandler.post( - () -> { - boolean success = true; - try { - mIntegrityFileManager.writeRules(version, ruleProvider, rules.getList()); - } catch (Exception e) { - Slog.e(TAG, "Error writing rules.", e); - success = false; - } - - if (DEBUG_INTEGRITY_COMPONENT) { - Slog.i( - TAG, - String.format( - "Successfully pushed rule set to version '%s' from '%s'", - version, ruleProvider)); - } - - Intent intent = new Intent(); - intent.putExtra(EXTRA_STATUS, success ? STATUS_SUCCESS : STATUS_FAILURE); - try { - statusReceiver.sendIntent( - mContext, - /* code= */ 0, - intent, - /* onFinished= */ null, - /* handler= */ null); - } catch (Exception e) { - Slog.e(TAG, "Error sending status feedback.", e); - } - }); } @Override @@ -209,21 +185,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); } - /** We will use the SHA256 digest of a package name if it is more than 32 bytes long. */ - private String getPackageNameNormalized(String packageName) { - if (packageName.length() <= 32) { - return packageName; - } - - try { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - byte[] hashBytes = messageDigest.digest(packageName.getBytes(StandardCharsets.UTF_8)); - return getHexDigest(hashBytes); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("SHA-256 algorithm not found", e); - } - } - private String getCallerPackageNameOrThrow(int callingUid) { String callerPackageName = getCallingRulePusherPackageName(callingUid); if (callerPackageName == null) { diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index e7735d8480f8..54e4f8e9a110 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -48,6 +48,9 @@ import static android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID; import static android.util.MathUtils.constrain; import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID; +import static com.android.internal.os.ProcfsMemoryUtil.getProcessCmdlines; +import static com.android.internal.os.ProcfsMemoryUtil.readCmdlineFromProcfs; +import static com.android.internal.os.ProcfsMemoryUtil.readMemorySnapshotFromProcfs; import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR; import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_STATS__GESTURE_SHORTCUT_TYPE__TRIPLE_TAP; import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_STATS__HARDWARE_SHORTCUT_TYPE__VOLUME_KEY; @@ -68,9 +71,6 @@ import static com.android.server.stats.Flags.addMobileBytesTransferByProcStatePu import static com.android.server.stats.Flags.applyNetworkStatsPollRateLimit; import static com.android.server.stats.pull.IonMemoryUtil.readProcessSystemIonHeapSizesFromDebugfs; import static com.android.server.stats.pull.IonMemoryUtil.readSystemIonHeapSizeFromDebugfs; -import static com.android.server.stats.pull.ProcfsMemoryUtil.getProcessCmdlines; -import static com.android.server.stats.pull.ProcfsMemoryUtil.readCmdlineFromProcfs; -import static com.android.server.stats.pull.ProcfsMemoryUtil.readMemorySnapshotFromProcfs; import static com.android.server.stats.pull.netstats.NetworkStatsUtils.fromPublicNetworkStats; import static com.android.server.stats.pull.netstats.NetworkStatsUtils.isAddEntriesSupported; @@ -209,6 +209,8 @@ import com.android.internal.os.KernelSingleProcessCpuThreadReader.ProcessCpuUsag import com.android.internal.os.LooperStats; import com.android.internal.os.PowerProfile; import com.android.internal.os.ProcessCpuTracker; +import com.android.internal.os.ProcfsMemoryUtil; +import com.android.internal.os.ProcfsMemoryUtil.MemorySnapshot; import com.android.internal.os.SelectedProcessCpuThreadReader; import com.android.internal.os.StoragedUidIoStatsReader; import com.android.internal.util.CollectionUtils; @@ -229,7 +231,6 @@ import com.android.server.power.stats.KernelWakelockReader; import com.android.server.power.stats.KernelWakelockStats; import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes; import com.android.server.stats.pull.IonMemoryUtil.IonAllocations; -import com.android.server.stats.pull.ProcfsMemoryUtil.MemorySnapshot; import com.android.server.stats.pull.netstats.NetworkStatsAccumulator; import com.android.server.stats.pull.netstats.NetworkStatsExt; import com.android.server.stats.pull.netstats.SubInfo; diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 4f71719006f5..567eca2f639a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2572,14 +2572,21 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { wpc.computeProcessActivityState(); } - void computeProcessActivityStateBatch() { + boolean computeProcessActivityStateBatch() { if (mActivityStateChangedProcs.isEmpty()) { - return; + return false; } + boolean changed = false; for (int i = mActivityStateChangedProcs.size() - 1; i >= 0; i--) { - mActivityStateChangedProcs.get(i).computeProcessActivityState(); + final WindowProcessController wpc = mActivityStateChangedProcs.get(i); + final int prevState = wpc.getActivityStateFlags(); + wpc.computeProcessActivityState(); + if (!changed && prevState != wpc.getActivityStateFlags()) { + changed = true; + } } mActivityStateChangedProcs.clear(); + return changed; } /** diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index ccd59969cec8..6cc4b1e6ede9 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -994,7 +994,9 @@ class BackNavigationController { // Ensure the final animation targets which hidden by transition could be visible. for (int i = 0; i < targets.size(); i++) { final WindowContainer wc = targets.get(i).mContainer; - wc.prepareSurfaces(); + if (wc.mSurfaceControl != null) { + wc.prepareSurfaces(); + } } // The pending builder could be cleared due to prepareSurfaces @@ -1328,16 +1330,13 @@ class BackNavigationController { if (!allWindowDrawn) { return; } - final SurfaceControl startingSurface = mOpenAnimAdaptor.mStartingSurface; - if (startingSurface != null && startingSurface.isValid()) { - startTransaction.addTransactionCommittedListener(Runnable::run, () -> { - synchronized (mWindowManagerService.mGlobalLock) { - if (mOpenAnimAdaptor != null) { - mOpenAnimAdaptor.cleanUpWindowlessSurface(true); - } + startTransaction.addTransactionCommittedListener(Runnable::run, () -> { + synchronized (mWindowManagerService.mGlobalLock) { + if (mOpenAnimAdaptor != null) { + mOpenAnimAdaptor.cleanUpWindowlessSurface(true); } - }); - } + } + }); } void clearBackAnimateTarget(boolean cancel) { diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 41a580438776..6707a27d83c8 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -105,6 +105,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserProperties; import android.content.res.Configuration; import android.graphics.Rect; +import android.graphics.Region; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; import android.hardware.power.Mode; @@ -153,6 +154,7 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.policy.PermissionPolicyInternal; import com.android.server.policy.WindowManagerPolicy; import com.android.server.utils.Slogf; +import com.android.server.wm.utils.RegionUtils; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -270,6 +272,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> private boolean mTaskLayersChanged = true; private int mTmpTaskLayerRank; private final RankTaskLayersRunnable mRankTaskLayersRunnable = new RankTaskLayersRunnable(); + private Region mTmpOccludingRegion; + private Region mTmpTaskRegion; private String mDestroyAllActivitiesReason; private final Runnable mDestroyAllActivitiesRunnable = new Runnable() { @@ -2921,6 +2925,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent> }); } + void invalidateTaskLayersAndUpdateOomAdjIfNeeded() { + mRankTaskLayersRunnable.mCheckUpdateOomAdj = true; + invalidateTaskLayers(); + } + void invalidateTaskLayers() { if (!mTaskLayersChanged) { mTaskLayersChanged = true; @@ -2938,13 +2947,18 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // Only rank for leaf tasks because the score of activity is based on immediate parent. forAllLeafTasks(task -> { final int oldRank = task.mLayerRank; - final ActivityRecord r = task.topRunningActivityLocked(); - if (r != null && r.isVisibleRequested()) { + final int oldRatio = task.mNonOccludedFreeformAreaRatio; + task.mNonOccludedFreeformAreaRatio = 0; + if (task.isVisibleRequested()) { task.mLayerRank = ++mTmpTaskLayerRank; + if (task.inFreeformWindowingMode()) { + computeNonOccludedFreeformAreaRatio(task); + } } else { task.mLayerRank = Task.LAYER_RANK_INVISIBLE; } - if (task.mLayerRank != oldRank) { + if (task.mLayerRank != oldRank + || task.mNonOccludedFreeformAreaRatio != oldRatio) { task.forAllActivities(activity -> { if (activity.hasProcess()) { mTaskSupervisor.onProcessActivityStateChanged(activity.app, @@ -2953,12 +2967,42 @@ class RootWindowContainer extends WindowContainer<DisplayContent> }); } }, true /* traverseTopToBottom */); - + if (mTmpOccludingRegion != null) { + mTmpOccludingRegion.setEmpty(); + } + boolean changed = false; if (!mTaskSupervisor.inActivityVisibilityUpdate()) { - mTaskSupervisor.computeProcessActivityStateBatch(); + changed = mTaskSupervisor.computeProcessActivityStateBatch(); + } + if (mRankTaskLayersRunnable.mCheckUpdateOomAdj) { + mRankTaskLayersRunnable.mCheckUpdateOomAdj = false; + if (changed) { + mService.updateOomAdj(); + } } } + /** This method is called for visible freeform task from top to bottom. */ + private void computeNonOccludedFreeformAreaRatio(@NonNull Task task) { + if (!com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode()) { + return; + } + if (mTmpOccludingRegion == null) { + mTmpOccludingRegion = new Region(); + mTmpTaskRegion = new Region(); + } + final Rect taskBounds = task.getBounds(); + mTmpTaskRegion.set(taskBounds); + // Exclude the area outside the display. + mTmpTaskRegion.op(task.mDisplayContent.getBounds(), Region.Op.INTERSECT); + // Exclude the area covered by the above tasks. + mTmpTaskRegion.op(mTmpOccludingRegion, Region.Op.DIFFERENCE); + task.mNonOccludedFreeformAreaRatio = 100 * RegionUtils.getAreaSize(mTmpTaskRegion) + / (taskBounds.width() * taskBounds.height()); + // Accumulate the occluding region for other visible tasks behind. + mTmpOccludingRegion.op(taskBounds, Region.Op.UNION); + } + void clearOtherAppTimeTrackers(AppTimeTracker except) { forAllActivities(r -> { if (r.appTimeTracker != except) { @@ -3862,6 +3906,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } private class RankTaskLayersRunnable implements Runnable { + boolean mCheckUpdateOomAdj; + @Override public void run() { synchronized (mService.mGlobalLock) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 4861341f830a..8a624b3945b6 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -432,6 +432,9 @@ class Task extends TaskFragment { // This number will be assigned when we evaluate OOM scores for all visible tasks. int mLayerRank = LAYER_RANK_INVISIBLE; + /** A 0~100 ratio to indicate the percentage of visible area on screen of a freeform task. */ + int mNonOccludedFreeformAreaRatio; + /* Unique identifier for this task. */ final int mTaskId; /* User for which this task was created. */ @@ -1207,6 +1210,28 @@ class Task extends TaskFragment { } @Override + void onResize() { + super.onResize(); + updateTaskLayerForFreeform(); + } + + @Override + void onMovedByResize() { + super.onMovedByResize(); + updateTaskLayerForFreeform(); + } + + private void updateTaskLayerForFreeform() { + if (!com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode()) { + return; + } + if (!isVisibleRequested() || !inFreeformWindowingMode()) { + return; + } + mRootWindowContainer.invalidateTaskLayersAndUpdateOomAdjIfNeeded(); + } + + @Override @Nullable ActivityRecord getTopResumedActivity() { if (!isLeafTask()) { @@ -3410,6 +3435,36 @@ class Task extends TaskFragment { info.requestedVisibleTypes = (windowState != null && Flags.enableFullyImmersiveInDesktop()) ? windowState.getRequestedVisibleTypes() : WindowInsets.Type.defaultVisible(); AppCompatUtils.fillAppCompatTaskInfo(this, info, top); + info.topActivityMainWindowFrame = calculateTopActivityMainWindowFrameForTaskInfo(top); + } + + /** + * Returns the top activity's main window frame if it doesn't match + * {@link ActivityRecord#getBounds() the top activity bounds}, or {@code null}, otherwise. + * + * @param top The top running activity of the task + */ + @Nullable + private static Rect calculateTopActivityMainWindowFrameForTaskInfo( + @Nullable ActivityRecord top) { + if (!Flags.betterSupportNonMatchParentActivity()) { + return null; + } + if (top == null) { + return null; + } + final WindowState mainWindow = top.findMainWindow(); + if (mainWindow == null) { + return null; + } + if (!mainWindow.mHaveFrame) { + return null; + } + final Rect windowFrame = mainWindow.getFrame(); + if (top.getBounds().equals(windowFrame)) { + return null; + } + return windowFrame; } /** @@ -5919,6 +5974,10 @@ class Task extends TaskFragment { pw.print(prefix); pw.print(" mLastNonFullscreenBounds="); pw.println(mLastNonFullscreenBounds); } + if (mNonOccludedFreeformAreaRatio != 0) { + pw.print(prefix); pw.print(" mNonOccludedFreeformAreaRatio="); + pw.println(mNonOccludedFreeformAreaRatio); + } if (isLeafTask()) { pw.println(prefix + " isSleeping=" + shouldSleepActivities()); printThisActivity(pw, getTopPausingActivity(), dumpPackage, false, diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 3ffeacfd5006..d6ba3123eb97 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -3027,7 +3027,11 @@ class TaskFragment extends WindowContainer<WindowContainer> { // The task order may be changed by finishIfPossible() for adjusting focus if there are // nested tasks, so add all activities into a list to avoid missed removals. final ArrayList<ActivityRecord> removingActivities = new ArrayList<>(); - forAllActivities((Consumer<ActivityRecord>) removingActivities::add); + forAllActivities((r) -> { + if (!r.finishing) { + removingActivities.add(r); + } + }); for (int i = removingActivities.size() - 1; i >= 0; --i) { final ActivityRecord r = removingActivities.get(i); if (withTransition && r.isVisible()) { diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 1a107c24a16a..7f0c33657144 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -121,6 +121,11 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio */ private static final int MAX_NUM_PERCEPTIBLE_FREEFORM = SystemProperties.getInt("persist.wm.max_num_perceptible_freeform", 1); + /** + * If the visible area percentage of a resumed freeform task is greater than or equal to this + * ratio, its process will have a higher priority. + */ + private static final int PERCEPTIBLE_FREEFORM_VISIBLE_RATIO = 90; private static final int MAX_RAPID_ACTIVITY_LAUNCH_COUNT = 200; private static final long RAPID_ACTIVITY_LAUNCH_MS = 500; @@ -1234,6 +1239,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio boolean hasResumedFreeform = false; int minTaskLayer = Integer.MAX_VALUE; int stateFlags = 0; + int nonOccludedRatio = 0; final boolean wasResumed = hasResumedActivity(); final boolean wasAnyVisible = (mActivityStateFlags & (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0; @@ -1261,6 +1267,8 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio stateFlags |= ACTIVITY_STATE_FLAG_RESUMED_SPLIT_SCREEN; } else if (windowingMode == WINDOWING_MODE_FREEFORM) { hasResumedFreeform = true; + nonOccludedRatio = + Math.max(task.mNonOccludedFreeformAreaRatio, nonOccludedRatio); } } if (minTaskLayer > 0) { @@ -1298,7 +1306,8 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio && com.android.window.flags.Flags.processPriorityPolicyForMultiWindowMode() // Exclude task layer 1 because it is already the top most. && minTaskLayer > 1) { - if (minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM) { + if (minTaskLayer <= 1 + MAX_NUM_PERCEPTIBLE_FREEFORM + || nonOccludedRatio >= PERCEPTIBLE_FREEFORM_VISIBLE_RATIO) { stateFlags |= ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM; } else { stateFlags |= ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE; diff --git a/services/core/java/com/android/server/wm/utils/RegionUtils.java b/services/core/java/com/android/server/wm/utils/RegionUtils.java index ce7776f270fd..ff23145fc6b9 100644 --- a/services/core/java/com/android/server/wm/utils/RegionUtils.java +++ b/services/core/java/com/android/server/wm/utils/RegionUtils.java @@ -81,4 +81,15 @@ public class RegionUtils { Collections.reverse(rects); rects.forEach(rectConsumer); } + + /** Returns the area size of the region. */ + public static int getAreaSize(Region region) { + final RegionIterator regionIterator = new RegionIterator(region); + int area = 0; + final Rect rect = new Rect(); + while (regionIterator.next(rect)) { + area += rect.width() * rect.height(); + } + return area; + } } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index e052f94b92ee..2461c1ce3b0e 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1504,6 +1504,8 @@ public final class SystemServer implements Dumpable { boolean isTv = context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_LEANBACK); + boolean isAutomotive = RoSystemFeatures.hasFeatureAutomotive(context); + boolean enableVrService = context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); @@ -1760,7 +1762,8 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } - if (android.security.Flags.aapmApi()) { + if (!isWatch && !isTv && !isAutomotive + && android.security.Flags.aapmApi()) { t.traceBegin("StartAdvancedProtectionService"); mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class); t.traceEnd(); @@ -3137,7 +3140,7 @@ public final class SystemServer implements Dumpable { }, WEBVIEW_PREPARATION); } - if (RoSystemFeatures.hasFeatureAutomotive(context)) { + if (isAutomotive) { t.traceBegin("StartCarServiceHelperService"); final SystemService cshs = mSystemServiceManager .startService(CAR_SERVICE_HELPER_SERVICE_CLASS); diff --git a/services/tests/PackageManagerServiceTests/appenumeration/Android.bp b/services/tests/PackageManagerServiceTests/appenumeration/Android.bp index f15e533fee2b..2f00a1bb3c8c 100644 --- a/services/tests/PackageManagerServiceTests/appenumeration/Android.bp +++ b/services/tests/PackageManagerServiceTests/appenumeration/Android.bp @@ -32,6 +32,7 @@ android_test { "androidx.test.runner", "truth", "Harrier", + "bedstead-multiuser", ], platform_apis: true, certificate: "platform", diff --git a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java index 70a2d4847ce7..48cebd7dcb04 100644 --- a/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java +++ b/services/tests/PackageManagerServiceTests/appenumeration/src/com/android/server/pm/test/appenumeration/CrossUserPackageVisibilityTests.java @@ -22,6 +22,7 @@ import static android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS; import static android.Manifest.permission.MOVE_PACKAGE; import static android.content.pm.PackageManager.MOVE_FAILED_DOESNT_EXIST; +import static com.android.bedstead.multiuser.MultiUserDeviceStateExtensionsKt.secondaryUser; import static com.android.compatibility.common.util.ShellUtils.runShellCommand; import static com.google.common.truth.Truth.assertThat; @@ -112,9 +113,9 @@ public class CrossUserPackageVisibilityTests { final UserReference primaryUser = sDeviceState.primaryUser(); if (primaryUser.id() == UserHandle.myUserId()) { mCurrentUser = primaryUser; - mOtherUser = sDeviceState.secondaryUser(); + mOtherUser = secondaryUser(sDeviceState); } else { - mCurrentUser = sDeviceState.secondaryUser(); + mCurrentUser = secondaryUser(sDeviceState); mOtherUser = primaryUser; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index fdf6b809fa85..9189c2f20d66 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -2224,6 +2224,8 @@ public final class DisplayPowerControllerTest { verify(mHolder.animator).animateTo(eq(DEFAULT_DOZE_BRIGHTNESS), /* linearSecondTarget= */ anyFloat(), eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false)); + // This brightness shouldn't be stored in the setting + verify(mHolder.brightnessSetting, never()).setBrightness(DEFAULT_DOZE_BRIGHTNESS); // The display device changes and the default doze brightness changes setUpDisplay(DISPLAY_ID, "new_unique_id", mHolder.display, mock(DisplayDevice.class), diff --git a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java index 9c6412b81b34..a2e6d4c7bfed 100644 --- a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java @@ -191,98 +191,6 @@ public class AppIntegrityManagerServiceImplTest { } @Test - public void updateRuleSet_notAuthorized() throws Exception { - makeUsSystemApp(); - Rule rule = - new Rule( - new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true), - Rule.DENY); - TestUtils.assertExpectException( - SecurityException.class, - "Only system packages specified in config_integrityRuleProviderPackages are" - + " allowed to call this method.", - () -> - mService.updateRuleSet( - VERSION, - new ParceledListSlice<>(Arrays.asList(rule)), - /* statusReceiver= */ null)); - } - - @Test - public void updateRuleSet_notSystemApp() throws Exception { - allowlistUsAsRuleProvider(); - makeUsSystemApp(false); - Rule rule = - new Rule( - new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true), - Rule.DENY); - TestUtils.assertExpectException( - SecurityException.class, - "Only system packages specified in config_integrityRuleProviderPackages are" - + " allowed to call this method.", - () -> - mService.updateRuleSet( - VERSION, - new ParceledListSlice<>(Arrays.asList(rule)), - /* statusReceiver= */ null)); - } - - @Test - public void updateRuleSet_authorized() throws Exception { - allowlistUsAsRuleProvider(); - makeUsSystemApp(); - Rule rule = - new Rule( - new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true), - Rule.DENY); - - // no SecurityException - mService.updateRuleSet( - VERSION, new ParceledListSlice<>(Arrays.asList(rule)), mock(IntentSender.class)); - } - - @Test - public void updateRuleSet_correctMethodCall() throws Exception { - allowlistUsAsRuleProvider(); - makeUsSystemApp(); - IntentSender mockReceiver = mock(IntentSender.class); - List<Rule> rules = - Arrays.asList( - new Rule( - IntegrityFormula.Application.packageNameEquals(PACKAGE_NAME), - Rule.DENY)); - - mService.updateRuleSet(VERSION, new ParceledListSlice<>(rules), mockReceiver); - runJobInHandler(); - - verify(mIntegrityFileManager).writeRules(VERSION, TEST_FRAMEWORK_PACKAGE, rules); - ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); - verify(mockReceiver).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any()); - assertEquals(STATUS_SUCCESS, intentCaptor.getValue().getIntExtra(EXTRA_STATUS, -1)); - } - - @Test - public void updateRuleSet_fail() throws Exception { - allowlistUsAsRuleProvider(); - makeUsSystemApp(); - doThrow(new IOException()).when(mIntegrityFileManager).writeRules(any(), any(), any()); - IntentSender mockReceiver = mock(IntentSender.class); - List<Rule> rules = - Arrays.asList( - new Rule( - IntegrityFormula.Application.packageNameEquals(PACKAGE_NAME), - Rule.DENY)); - - mService.updateRuleSet(VERSION, new ParceledListSlice<>(rules), mockReceiver); - runJobInHandler(); - - verify(mIntegrityFileManager).writeRules(VERSION, TEST_FRAMEWORK_PACKAGE, rules); - ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); - verify(mockReceiver).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any()); - assertEquals(STATUS_FAILURE, intentCaptor.getValue().getIntExtra(EXTRA_STATUS, -1)); - } - - @Test public void broadcastReceiverRegistration() throws Exception { allowlistUsAsRuleProvider(); makeUsSystemApp(); diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java index cf1dcd0515d1..7e8bd38fb6a9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java @@ -285,6 +285,52 @@ public class RootWindowContainerTests extends WindowTestsBase { } @Test + public void testTaskLayerRankFreeform() { + mSetFlagsRule.enableFlags(com.android.window.flags.Flags + .FLAG_PROCESS_PRIORITY_POLICY_FOR_MULTI_WINDOW_MODE); + final Task[] freeformTasks = new Task[3]; + final WindowProcessController[] processes = new WindowProcessController[3]; + for (int i = 0; i < freeformTasks.length; i++) { + freeformTasks[i] = new TaskBuilder(mSupervisor) + .setWindowingMode(WINDOWING_MODE_FREEFORM).setCreateActivity(true).build(); + final ActivityRecord r = freeformTasks[i].getTopMostActivity(); + r.setState(RESUMED, "test"); + processes[i] = r.app; + } + resizeDisplay(mDisplayContent, 2400, 2000); + // --------- + // | 2 | 1 | + // --------- + // | 0 | | + // --------- + freeformTasks[2].setBounds(0, 0, 1000, 1000); + freeformTasks[1].setBounds(1000, 0, 2000, 1000); + freeformTasks[0].setBounds(0, 1000, 1000, 2000); + mRootWindowContainer.rankTaskLayers(); + assertEquals(1, freeformTasks[2].mLayerRank); + assertEquals(2, freeformTasks[1].mLayerRank); + assertEquals(3, freeformTasks[0].mLayerRank); + assertFalse("Top doesn't need perceptible hint", (processes[2].getActivityStateFlags() + & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0); + assertTrue((processes[1].getActivityStateFlags() + & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0); + assertTrue((processes[0].getActivityStateFlags() + & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0); + + // Make task2 occlude half of task0. + clearInvocations(mAtm); + freeformTasks[2].setBounds(0, 0, 1000, 1500); + waitHandlerIdle(mWm.mH); + // The process of task0 will demote from perceptible to visible. + final int stateFlags0 = processes[0].getActivityStateFlags(); + assertTrue((stateFlags0 + & WindowProcessController.ACTIVITY_STATE_FLAG_VISIBLE_MULTI_WINDOW_MODE) != 0); + assertFalse((stateFlags0 + & WindowProcessController.ACTIVITY_STATE_FLAG_PERCEPTIBLE_FREEFORM) != 0); + verify(mAtm).updateOomAdj(); + } + + @Test public void testForceStopPackage() { final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); final ActivityRecord activity = task.getTopMostActivity(); diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 44de65a009ff..79b3a7c4de65 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -582,6 +582,22 @@ public final class SatelliteManager { "android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED"; /** + * Meta-data represents whether the application supports P2P SMS over carrier roaming satellite + * which needs manual trigger to connect to satellite. The messaging applications that supports + * P2P SMS over carrier roaming satellites should add the following in their AndroidManifest. + * {@code + * <application + * <meta-data + * android:name="android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT" + * android:value="true"/> + * </application> + * } + * @hide + */ + public static final String METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT = + "android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT"; + + /** * Request to enable or disable the satellite modem and demo mode. * If satellite modem and cellular modem cannot work concurrently, * then this will disable the cellular modem if satellite modem is enabled, 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 c6855b4a97b4..4ac567cc3937 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 @@ -285,7 +285,11 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val displayRect = getDisplayRect(wmHelper) - val endX = if (isLeft) displayRect.left else displayRect.right + val endX = if (isLeft) { + displayRect.left + SNAP_RESIZE_DRAG_INSET + } else { + displayRect.right - SNAP_RESIZE_DRAG_INSET + } val endY = displayRect.centerY() / 2 // drag the window to snap resize @@ -391,6 +395,7 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : private companion object { val TIMEOUT: Duration = Duration.ofSeconds(3) + const val SNAP_RESIZE_DRAG_INSET: Int = 5 // inset to avoid dragging to display edge const val CAPTION: String = "desktop_mode_caption" const val MAXIMIZE_BUTTON_VIEW: String = "maximize_button_view" const val MAXIMIZE_MENU: String = "maximize_menu" diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 7526737f60bf..787ae06cd856 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -799,6 +799,27 @@ class KeyGestureControllerTests { } @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) + fun testMoveToNextDisplay() { + val keyGestureController = KeyGestureController(context, testLooper.looper) + testKeyGestureInternal( + keyGestureController, + TestData( + "META + CTRL + D -> Move a task to next display", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_D + ), + KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY, + intArrayOf(KeyEvent.KEYCODE_D), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test fun testCapsLockPressNotified() { val keyGestureController = KeyGestureController(context, testLooper.looper) val listener = KeyGestureEventListener() diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java index 6f3deab1d4fa..2692e12c57ed 100644 --- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java @@ -40,7 +40,6 @@ import android.tools.traces.io.ResultWriter; import android.tools.traces.monitors.PerfettoTraceMonitor; import android.tools.traces.protolog.ProtoLogTrace; import android.tracing.perfetto.DataSource; -import android.util.proto.ProtoInputStream; import androidx.test.platform.app.InstrumentationRegistry; @@ -74,7 +73,7 @@ import java.util.concurrent.atomic.AtomicInteger; @SuppressWarnings("ConstantConditions") @Presubmit @RunWith(JUnit4.class) -public class PerfettoProtoLogImplTest { +public class ProcessedPerfettoProtoLogImplTest { private static final String TEST_PROTOLOG_DATASOURCE_NAME = "test.android.protolog"; private static final String MOCK_VIEWER_CONFIG_FILE = "my/mock/viewer/config/file.pb"; private final File mTracingDirectory = InstrumentationRegistry.getInstrumentation() @@ -100,7 +99,7 @@ public class PerfettoProtoLogImplTest { private static ProtoLogViewerConfigReader sReader; - public PerfettoProtoLogImplTest() throws IOException { + public ProcessedPerfettoProtoLogImplTest() throws IOException { } @BeforeClass @@ -151,7 +150,8 @@ public class PerfettoProtoLogImplTest { ViewerConfigInputStreamProvider viewerConfigInputStreamProvider = Mockito.mock( ViewerConfigInputStreamProvider.class); Mockito.when(viewerConfigInputStreamProvider.getInputStream()) - .thenAnswer(it -> new ProtoInputStream(sViewerConfigBuilder.build().toByteArray())); + .thenAnswer(it -> new AutoClosableProtoInputStream( + sViewerConfigBuilder.build().toByteArray())); sCacheUpdater = () -> {}; sReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); @@ -165,21 +165,16 @@ public class PerfettoProtoLogImplTest { throw new RuntimeException( "Unexpected viewer config file path provided"); } - return new ProtoInputStream(sViewerConfigBuilder.build().toByteArray()); + return new AutoClosableProtoInputStream(sViewerConfigBuilder.build().toByteArray()); }); }; sProtoLogConfigurationService = new ProtoLogConfigurationServiceImpl(dataSourceBuilder, tracer); - if (android.tracing.Flags.clientSideProtoLogging()) { - sProtoLog = new PerfettoProtoLogImpl( - MOCK_VIEWER_CONFIG_FILE, sReader, () -> sCacheUpdater.run(), - TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService); - } else { - sProtoLog = new PerfettoProtoLogImpl( - viewerConfigInputStreamProvider, sReader, () -> sCacheUpdater.run(), - TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService); - } + sProtoLog = new ProcessedPerfettoProtoLogImpl( + MOCK_VIEWER_CONFIG_FILE, viewerConfigInputStreamProvider, sReader, + () -> sCacheUpdater.run(), TestProtoLogGroup.values(), dataSourceBuilder, + sProtoLogConfigurationService); busyWaitForDataSourceRegistration(TEST_PROTOLOG_DATASOURCE_NAME); } @@ -398,18 +393,17 @@ public class PerfettoProtoLogImplTest { } @Test - public void log_logcatEnabledNoMessage() { + public void log_logcatEnabledNoMessageThrows() { when(sReader.getViewerString(anyLong())).thenReturn(null); PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); - implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, - new Object[]{5}); - - verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( - LogLevel.INFO), eq("UNKNOWN MESSAGE args = (5)")); - verify(sReader).getViewerString(eq(1234L)); + var assertion = assertThrows(RuntimeException.class, () -> + implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + new Object[]{5})); + Truth.assertThat(assertion).hasMessageThat() + .contains("Failed to decode message for logcat"); } @Test @@ -539,16 +533,12 @@ public class PerfettoProtoLogImplTest { PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME) .build(); - long before; - long after; try { traceMonitor.start(); - before = SystemClock.elapsedRealtimeNanos(); sProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, 0b01100100, new Object[]{"test", 1, 0.1, true}); - after = SystemClock.elapsedRealtimeNanos(); } finally { traceMonitor.stop(mWriter); } @@ -606,7 +596,8 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(stacktrace).doesNotContain(DataSource.class.getSimpleName() + ".java"); Truth.assertThat(stacktrace) .doesNotContain(ProtoLogImpl.class.getSimpleName() + ".java"); - Truth.assertThat(stacktrace).contains(PerfettoProtoLogImplTest.class.getSimpleName()); + Truth.assertThat(stacktrace) + .contains(ProcessedPerfettoProtoLogImplTest.class.getSimpleName()); Truth.assertThat(stacktrace).contains("stackTraceTrimmed"); } diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java index 8ecddaa76216..3d1e208189b0 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java @@ -47,12 +47,12 @@ public class ProtoLogTest { } @Test - public void throwOnRegisteringDuplicateGroup() { - final var assertion = assertThrows(RuntimeException.class, - () -> ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2)); + public void deduplicatesRegisteringDuplicateGroup() { + ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2); - Truth.assertThat(assertion).hasMessageThat().contains("" + TEST_GROUP_1.getId()); - Truth.assertThat(assertion).hasMessageThat().contains("duplicate"); + final var instance = ProtoLog.getSingleInstance(); + Truth.assertThat(instance.getRegisteredGroups()) + .containsExactly(TEST_GROUP_1, TEST_GROUP_2); } @Test diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java index d78ced161eaf..9e029a8d5e57 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java @@ -19,9 +19,12 @@ package com.android.internal.protolog; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import android.os.Build; import android.platform.test.annotations.Presubmit; -import android.util.proto.ProtoInputStream; +import com.google.common.truth.Truth; + +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,6 +32,8 @@ import org.junit.runners.JUnit4; import perfetto.protos.ProtologCommon; +import java.io.File; + @Presubmit @RunWith(JUnit4.class) public class ProtoLogViewerConfigReaderTest { @@ -83,7 +88,7 @@ public class ProtoLogViewerConfigReaderTest { ).build().toByteArray(); private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider = - () -> new ProtoInputStream(TEST_VIEWER_CONFIG); + () -> new AutoClosableProtoInputStream(TEST_VIEWER_CONFIG); private ProtoLogViewerConfigReader mConfig; @@ -123,6 +128,31 @@ public class ProtoLogViewerConfigReaderTest { } @Test + public void viewerConfigIsOnDevice() { + Assume.assumeFalse(Build.FINGERPRINT.contains("robolectric")); + + final String[] viewerConfigPaths; + if (android.tracing.Flags.perfettoProtologTracing()) { + viewerConfigPaths = new String[] { + "/system_ext/etc/wmshell.protolog.pb", + "/system/etc/core.protolog.pb", + }; + } else { + viewerConfigPaths = new String[] { + "/system_ext/etc/wmshell.protolog.json.gz", + "/system/etc/protolog.conf.json.gz", + }; + } + + for (final var viewerConfigPath : viewerConfigPaths) { + File f = new File(viewerConfigPath); + + Truth.assertWithMessage(f.getAbsolutePath() + " exists").that(f.exists()).isTrue(); + } + + } + + @Test public void loadUnloadAndReloadViewerConfig() { loadViewerConfig(); unloadViewerConfig(); |