diff options
187 files changed, 5894 insertions, 1218 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 1ae9ada41338..3790a96dd2ba 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -21,6 +21,7 @@ aconfig_declarations_group { java_aconfig_libraries: [ // !!! KEEP THIS LIST ALPHABETICAL !!! "aconfig_mediacodec_flags_java_lib", + "aconfig_trade_in_mode_flags_java_lib", "android-sdk-flags-java", "android.adaptiveauth.flags-aconfig-java", "android.app.appfunctions.flags-aconfig-java", @@ -1559,6 +1560,10 @@ java_aconfig_library { name: "android.crashrecovery.flags-aconfig-java", aconfig_declarations: "android.crashrecovery.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], + apex_available: [ + "//apex_available:platform", + "com.android.crashrecovery", + ], } java_aconfig_library { diff --git a/Android.bp b/Android.bp index 252aeef079d2..b569df254c3f 100644 --- a/Android.bp +++ b/Android.bp @@ -109,7 +109,7 @@ filegroup { ":android.hardware.radio.voice-V3-java-source", ":android.hardware.security.keymint-V3-java-source", ":android.hardware.security.secureclock-V1-java-source", - ":android.hardware.thermal-V2-java-source", + ":android.hardware.thermal-V3-java-source", ":android.hardware.tv.tuner-V3-java-source", ":android.security.apc-java-source", ":android.security.authorization-java-source", diff --git a/core/api/current.txt b/core/api/current.txt index 09fef2530ec9..73b35b211f25 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -16369,6 +16369,7 @@ package android.graphics { field public static final int UNKNOWN = 0; // 0x0 field public static final int Y8 = 538982489; // 0x20203859 field public static final int YCBCR_P010 = 54; // 0x36 + field @FlaggedApi("android.media.codec.p210_format_support") public static final int YCBCR_P210 = 60; // 0x3c field public static final int YUV_420_888 = 35; // 0x23 field public static final int YUV_422_888 = 39; // 0x27 field public static final int YUV_444_888 = 40; // 0x28 @@ -18718,6 +18719,7 @@ package android.hardware { field public static final long USAGE_VIDEO_ENCODE = 65536L; // 0x10000L field public static final int YCBCR_420_888 = 35; // 0x23 field public static final int YCBCR_P010 = 54; // 0x36 + field @FlaggedApi("android.media.codec.p210_format_support") public static final int YCBCR_P210 = 60; // 0x3c } @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public final class OverlayProperties implements android.os.Parcelable { @@ -22985,6 +22987,7 @@ package android.media { field public static final int COLOR_FormatYUV444Flexible = 2135181448; // 0x7f444888 field @Deprecated public static final int COLOR_FormatYUV444Interleaved = 29; // 0x1d field public static final int COLOR_FormatYUVP010 = 54; // 0x36 + field @FlaggedApi("android.media.codec.p210_format_support") public static final int COLOR_FormatYUVP210 = 60; // 0x3c field @Deprecated public static final int COLOR_QCOM_FormatYUV420SemiPlanar = 2141391872; // 0x7fa30c00 field @Deprecated public static final int COLOR_TI_FormatYUV420PackedSemiPlanar = 2130706688; // 0x7f000100 field public static final String FEATURE_AdaptivePlayback = "adaptive-playback"; @@ -61772,7 +61775,7 @@ package android.window { public final class BackEvent { ctor public BackEvent(float, float, float, int); ctor @FlaggedApi("com.android.window.flags.predictive_back_timestamp_api") public BackEvent(float, float, float, int, long); - method @FlaggedApi("com.android.window.flags.predictive_back_timestamp_api") public long getFrameTime(); + method @FlaggedApi("com.android.window.flags.predictive_back_timestamp_api") public long getFrameTimeMillis(); method @FloatRange(from=0, to=1) public float getProgress(); method public int getSwipeEdge(); method public float getTouchX(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 207f4b57e8bf..f04df2f5e3fb 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -9429,6 +9429,7 @@ package android.media.tv.tuner.frontend { method public int getSignalStrength(); method public int getSnr(); method public int getSpectralInversion(); + method @FlaggedApi("android.media.tv.flags.tuner_w_apis") @NonNull public android.media.tv.tuner.frontend.StandardExt getStandardExt(); method @NonNull public int[] getStreamIds(); method public int getSymbolRate(); method @IntRange(from=0, to=65535) public int getSystemId(); @@ -9483,6 +9484,7 @@ package android.media.tv.tuner.frontend { field public static final int FRONTEND_STATUS_TYPE_SIGNAL_STRENGTH = 6; // 0x6 field public static final int FRONTEND_STATUS_TYPE_SNR = 1; // 0x1 field public static final int FRONTEND_STATUS_TYPE_SPECTRAL = 10; // 0xa + field @FlaggedApi("android.media.tv.flags.tuner_w_apis") public static final int FRONTEND_STATUS_TYPE_STANDARD_EXT = 47; // 0x2f field public static final int FRONTEND_STATUS_TYPE_STREAM_IDS = 39; // 0x27 field public static final int FRONTEND_STATUS_TYPE_SYMBOL_RATE = 7; // 0x7 field public static final int FRONTEND_STATUS_TYPE_T2_SYSTEM_ID = 29; // 0x1d @@ -9774,6 +9776,11 @@ package android.media.tv.tuner.frontend { method public default void onUnlocked(); } + @FlaggedApi("android.media.tv.flags.tuner_w_apis") public final class StandardExt { + method public int getDvbsStandardExt(); + method public int getDvbtStandardExt(); + } + } package android.media.voice { diff --git a/core/java/Android.bp b/core/java/Android.bp index d12e1bf77e17..9875efe04361 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -345,6 +345,13 @@ filegroup { } filegroup { + name: "service-crashrecovery-shared-srcs", + srcs: [ + "android/util/IndentingPrintWriter.java", + ], +} + +filegroup { name: "incremental_aidl", srcs: [ "android/os/incremental/IIncrementalServiceConnector.aidl", diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index ffb920b907ab..f880901429f7 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -501,7 +501,7 @@ interface IActivityManager { in String shareDescription); void requestInteractiveBugReport(); - void requestBugReportWithExtraAttachment(in Uri extraAttachment); + void requestBugReportWithExtraAttachments(in List<Uri> extraAttachment); void requestFullBugReport(); void requestRemoteBugReport(long nonce); boolean launchBugReportHandlerApp(); diff --git a/core/java/android/appwidget/OWNERS b/core/java/android/appwidget/OWNERS index 191083303769..0e85d5bd7a27 100644 --- a/core/java/android/appwidget/OWNERS +++ b/core/java/android/appwidget/OWNERS @@ -3,3 +3,5 @@ sihua@google.com pinyaoting@google.com suprabh@google.com sunnygoyal@google.com +zakcohen@google.com +shamalip@google.com diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig index ac9263c2cab5..3839b5fa2599 100644 --- a/core/java/android/appwidget/flags.aconfig +++ b/core/java/android/appwidget/flags.aconfig @@ -74,3 +74,12 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "use_smaller_app_widget_radius" + namespace: "app_widgets" + description: "Updates system corner radius for app widgets to 24.dp instead of 28.dp" + bug: "373351337" + is_exported: true + is_fixed_read_only: true +} diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java index 93958443c01b..44115c832c17 100644 --- a/core/java/android/hardware/HardwareBuffer.java +++ b/core/java/android/hardware/HardwareBuffer.java @@ -66,6 +66,7 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable { DS_FP32UI8, S_UI8, YCBCR_P010, + YCBCR_P210, R_8, R_16, RG_1616, @@ -111,6 +112,16 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable { * little-endian value, with the lower 6 bits set to zero. */ public static final int YCBCR_P010 = 0x36; + /** + * <p>Android YUV P210 format.</p> + * + * P210 is a 4:2:2 YCbCr semiplanar format comprised of a WxH Y plane + * followed by a WxH CbCr plane. Each sample is represented by a 16-bit + * little-endian value, with the lower 6 bits set to zero. + */ + @FlaggedApi(android.media.codec.Flags.FLAG_P210_FORMAT_SUPPORT) + public static final int YCBCR_P210 = 0x3c; + /** Format: 8 bits red */ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) public static final int R_8 = 0x38; diff --git a/core/java/android/os/ITradeInMode.aidl b/core/java/android/os/ITradeInMode.aidl new file mode 100644 index 000000000000..f15954d14d0e --- /dev/null +++ b/core/java/android/os/ITradeInMode.aidl @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +/** @hide */ +interface ITradeInMode { + /** + * Enable adb in limited-privilege trade-in mode. Returns true if trade-in + * mode was enabled. + * + * Trade-in mode can be enabled if the following conditions are all true: + * ro.debuggable is 0. + * Settings.Global.ADB_ENABLED is 0. + * Settings.Global.USER_SETUP_COMPLETE is 0. + * Settings.Secure.DEVICE_PROVISIONED is 0. + * + * It is stopped automatically when any of the following conditions become + * true: + * + * Settings.Global.USER_SETUP_COMPLETE is 1. + * Settings.Secure.DEVICE_PROVISIONED is 1. + * A change in network configuration occurs. + * An account is added. + * + * ENTER_TRADE_IN_MODE permission is required. + */ + boolean start(); + + /** + * Returns whether evaluation mode is allowed on this device. It will return + * false if any kind of device protection (such as FRP) is detected. + * + * ENTER_TRADE_IN_MODE permission is required. + */ + boolean isEvaluationModeAllowed(); + + /** + * Enable full adb access and provision the device. This forces a factory + * reset on the next boot. + * + * This will return false if start() was not called, if factory reset + * protection is active, or if trade-in mode was disabled due to any of the + * conditions listed above for start(). + * + * ENTER_TRADE_IN_MODE permission is required. + */ + boolean enterEvaluationMode(); +} diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index c7cc653f4178..9c83bc2c88ec 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -91,6 +91,14 @@ flag { } flag { + name: "allow_thermal_thresholds_callback" + is_exported: true + namespace: "game" + description: "Enable thermal threshold callback" + bug: "360486877" +} + +flag { name: "android_os_build_vanilla_ice_cream" is_exported: true namespace: "build" diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index bfefba5b36e6..3df7ff9e8165 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -282,3 +282,12 @@ flag { description: "This fixed read-only flag is used to enable platform support for Skin Temperature." bug: "369872443" } + +flag { + name: "platform_oxygen_saturation_enabled" + is_fixed_read_only: true + is_exported: true + namespace: "android_health_services" + description: "This fixed read-only flag is used to enable platform support for Oxygen Saturation (SpO2)." + bug: "369873227" +} diff --git a/core/java/android/print/IPrintDocumentAdapter.aidl b/core/java/android/print/IPrintDocumentAdapter.aidl index 8f33e0b2c003..9d384fba874d 100644 --- a/core/java/android/print/IPrintDocumentAdapter.aidl +++ b/core/java/android/print/IPrintDocumentAdapter.aidl @@ -37,5 +37,4 @@ oneway interface IPrintDocumentAdapter { void write(in PageRange[] pages, in ParcelFileDescriptor fd, IWriteResultCallback callback, int sequence); void finish(); - void kill(String reason); } diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java index ef274a56e1d3..1b1554f8192d 100644 --- a/core/java/android/print/PrintManager.java +++ b/core/java/android/print/PrintManager.java @@ -946,17 +946,6 @@ public final class PrintManager { } @Override - public void kill(String reason) { - synchronized (mLock) { - // If destroyed the handler is null. - if (!isDestroyedLocked()) { - mHandler.obtainMessage(MyHandler.MSG_ON_KILL, - reason).sendToTarget(); - } - } - } - - @Override public void onActivityPaused(Activity activity) { /* do nothing */ } @@ -1118,15 +1107,6 @@ public final class PrintManager { } } break; - case MSG_ON_KILL: { - if (DEBUG) { - Log.i(LOG_TAG, "onKill()"); - } - - String reason = (String) message.obj; - throw new RuntimeException(reason); - } - default: { throw new IllegalArgumentException("Unknown message: " + message.what); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 8a784eb47ef1..83c599e57de9 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5472,6 +5472,14 @@ public final class Settings { public static final String VOLUME_MASTER = "volume_master"; /** + * The mapping of input device to its input gain index. + * + * @hide + */ + @Readable + public static final String INPUT_GAIN_INDEX_SETTINGS = "input_gain_index_settings"; + + /** * Master mono (int 1 = mono, 0 = normal). * * @hide @@ -8390,7 +8398,6 @@ public final class Settings { @Readable public static final String LOCK_SCREEN_LOCK_AFTER_TIMEOUT = "lock_screen_lock_after_timeout"; - /** * This preference contains the string that shows for owner info on LockScreen. * @hide diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java index d90455ab971d..2ca62a0725df 100644 --- a/core/java/android/view/inputmethod/ImeTracker.java +++ b/core/java/android/view/inputmethod/ImeTracker.java @@ -220,6 +220,7 @@ public interface ImeTracker { PHASE_WM_POSTING_CHANGED_IME_VISIBILITY, PHASE_WM_INVOKING_IME_REQUESTED_LISTENER, PHASE_CLIENT_ALREADY_HIDDEN, + PHASE_CLIENT_VIEW_HANDLER_AVAILABLE, }) @Retention(RetentionPolicy.SOURCE) @interface Phase {} @@ -424,6 +425,11 @@ public interface ImeTracker { ImeProtoEnums.PHASE_WM_INVOKING_IME_REQUESTED_LISTENER; /** IME is requested to be hidden, but already hidden. Don't hide to avoid another animation. */ int PHASE_CLIENT_ALREADY_HIDDEN = ImeProtoEnums.PHASE_CLIENT_ALREADY_HIDDEN; + /** + * The view's handler is needed to check if we're running on a different thread. We can't + * continue without. + */ + int PHASE_CLIENT_VIEW_HANDLER_AVAILABLE = ImeProtoEnums.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE; /** * Called when an IME request is started. diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 47fc43735c4d..9001251914d6 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -2591,6 +2591,17 @@ public final class InputMethodManager { // TODO(b/322992891) handle case of HIDE_IMPLICIT_ONLY final var viewRootImpl = servedView.getViewRootImpl(); if (viewRootImpl != null) { + Handler vh = servedView.getHandler(); + if (vh == null) { + // If the view doesn't have a handler, something has changed out from + // under us. The current input has been closed before (from checkFocus). + ImeTracker.forLogging().onFailed(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + return false; + } + ImeTracker.forLogging().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE); + if (resultReceiver != null) { final boolean imeReqVisible = (viewRootImpl.getInsetsController().getRequestedVisibleTypes() @@ -2599,7 +2610,15 @@ public final class InputMethodManager { !imeReqVisible ? InputMethodManager.RESULT_UNCHANGED_HIDDEN : InputMethodManager.RESULT_HIDDEN, null); } - viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime()); + if (vh.getLooper() != Looper.myLooper()) { + // The view is running on a different thread than our own, so + // we need to reschedule our work for over there. + if (DEBUG) Log.v(TAG, "Hiding soft input: reschedule to view thread"); + vh.post(() -> viewRootImpl.getInsetsController().hide( + WindowInsets.Type.ime())); + } else { + viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime()); + } } return true; } else { diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java index 90fac361c966..1b9235b21369 100644 --- a/core/java/android/window/BackEvent.java +++ b/core/java/android/window/BackEvent.java @@ -58,7 +58,7 @@ public final class BackEvent { private final float mTouchX; private final float mTouchY; private final float mProgress; - private final long mFrameTime; + private final long mFrameTimeMillis; @SwipeEdge private final int mSwipeEdge; @@ -68,7 +68,7 @@ public final class BackEvent { if (predictiveBackTimestampApi()) { return new BackEvent(backMotionEvent.getTouchX(), backMotionEvent.getTouchY(), backMotionEvent.getProgress(), backMotionEvent.getSwipeEdge(), - backMotionEvent.getFrameTime()); + backMotionEvent.getFrameTimeMillis()); } else { return new BackEvent(backMotionEvent.getTouchX(), backMotionEvent.getTouchY(), backMotionEvent.getProgress(), backMotionEvent.getSwipeEdge()); @@ -88,7 +88,7 @@ public final class BackEvent { mTouchY = touchY; mProgress = progress; mSwipeEdge = swipeEdge; - mFrameTime = System.nanoTime() / TimeUtils.NANOS_PER_MS; + mFrameTimeMillis = System.nanoTime() / TimeUtils.NANOS_PER_MS; } /** @@ -98,16 +98,16 @@ public final class BackEvent { * @param touchY Absolute Y location of the touch point of this event. * @param progress Value between 0 and 1 on how far along the back gesture is. * @param swipeEdge Indicates which edge the swipe starts from. - * @param frameTime frame time of the back event. + * @param frameTimeMillis frame time of the back event. */ @FlaggedApi(FLAG_PREDICTIVE_BACK_TIMESTAMP_API) public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge, - long frameTime) { + long frameTimeMillis) { mTouchX = touchX; mTouchY = touchY; mProgress = progress; mSwipeEdge = swipeEdge; - mFrameTime = frameTime; + mFrameTimeMillis = frameTimeMillis; } /** @@ -160,8 +160,8 @@ public final class BackEvent { * Returns the frameTime of the BackEvent in milliseconds. Useful for calculating velocity. */ @FlaggedApi(FLAG_PREDICTIVE_BACK_TIMESTAMP_API) - public long getFrameTime() { - return mFrameTime; + public long getFrameTimeMillis() { + return mFrameTimeMillis; } @Override @@ -177,7 +177,7 @@ public final class BackEvent { && mTouchY == that.mTouchY && mProgress == that.mProgress && mSwipeEdge == that.mSwipeEdge - && mFrameTime == that.mFrameTime; + && mFrameTimeMillis == that.mFrameTimeMillis; } @Override @@ -187,7 +187,7 @@ public final class BackEvent { + ", mTouchY=" + mTouchY + ", mProgress=" + mProgress + ", mSwipeEdge=" + mSwipeEdge - + ", mFrameTime=" + mFrameTime + "ms" + + ", mFrameTimeMillis=" + mFrameTimeMillis + "}"; } } diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java index a8ec4eeb039a..cc2afbc6aaa3 100644 --- a/core/java/android/window/BackMotionEvent.java +++ b/core/java/android/window/BackMotionEvent.java @@ -33,7 +33,7 @@ import android.view.RemoteAnimationTarget; public final class BackMotionEvent implements Parcelable { private final float mTouchX; private final float mTouchY; - private final long mFrameTime; + private final long mFrameTimeMillis; private final float mProgress; private final boolean mTriggerBack; @@ -49,7 +49,7 @@ public final class BackMotionEvent implements Parcelable { * * @param touchX Absolute X location of the touch point of this event. * @param touchY Absolute Y location of the touch point of this event. - * @param frameTime Event time of the corresponding touch event. + * @param frameTimeMillis Event time of the corresponding touch event. * @param progress Value between 0 and 1 on how far along the back gesture is. * @param triggerBack Indicates whether the back arrow is in the triggered state or not * @param swipeEdge Indicates which edge the swipe starts from. @@ -59,14 +59,14 @@ public final class BackMotionEvent implements Parcelable { public BackMotionEvent( float touchX, float touchY, - long frameTime, + long frameTimeMillis, float progress, boolean triggerBack, @BackEvent.SwipeEdge int swipeEdge, @Nullable RemoteAnimationTarget departingAnimationTarget) { mTouchX = touchX; mTouchY = touchY; - mFrameTime = frameTime; + mFrameTimeMillis = frameTimeMillis; mProgress = progress; mTriggerBack = triggerBack; mSwipeEdge = swipeEdge; @@ -80,7 +80,7 @@ public final class BackMotionEvent implements Parcelable { mTriggerBack = in.readBoolean(); mSwipeEdge = in.readInt(); mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR); - mFrameTime = in.readLong(); + mFrameTimeMillis = in.readLong(); } @NonNull @@ -109,7 +109,7 @@ public final class BackMotionEvent implements Parcelable { dest.writeBoolean(mTriggerBack); dest.writeInt(mSwipeEdge); dest.writeTypedObject(mDepartingAnimationTarget, flags); - dest.writeLong(mFrameTime); + dest.writeLong(mFrameTimeMillis); } /** @@ -156,8 +156,8 @@ public final class BackMotionEvent implements Parcelable { /** * Returns the frame time of the BackMotionEvent in milliseconds. */ - public long getFrameTime() { - return mFrameTime; + public long getFrameTimeMillis() { + return mFrameTimeMillis; } /** @@ -175,7 +175,7 @@ public final class BackMotionEvent implements Parcelable { return "BackMotionEvent{" + "mTouchX=" + mTouchX + ", mTouchY=" + mTouchY - + ", mFrameTime=" + mFrameTime + "ms" + + ", mFrameTimeMillis=" + mFrameTimeMillis + ", mProgress=" + mProgress + ", mTriggerBack=" + mTriggerBack + ", mSwipeEdge=" + mSwipeEdge diff --git a/core/java/android/window/BackTouchTracker.java b/core/java/android/window/BackTouchTracker.java index 39a30253adbd..4908068d51e4 100644 --- a/core/java/android/window/BackTouchTracker.java +++ b/core/java/android/window/BackTouchTracker.java @@ -151,7 +151,7 @@ public class BackTouchTracker { return new BackMotionEvent( /* touchX = */ mInitTouchX, /* touchY = */ mInitTouchY, - /* frameTime = */ 0, + /* frameTimeMillis = */ 0, /* progress = */ 0, /* triggerBack = */ mTriggerBack, /* swipeEdge = */ mSwipeEdge, @@ -236,7 +236,7 @@ public class BackTouchTracker { return new BackMotionEvent( /* touchX = */ mLatestTouchX, /* touchY = */ mLatestTouchY, - /* frameTime = */ 0, + /* frameTimeMillis = */ 0, /* progress = */ progress, /* triggerBack = */ mTriggerBack, /* swipeEdge = */ mSwipeEdge, diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java index 8db1f9509757..bd01899a649b 100644 --- a/core/java/android/window/ImeOnBackInvokedDispatcher.java +++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java @@ -238,7 +238,7 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc try { long frameTime = 0; if (predictiveBackTimestampApi()) { - frameTime = backEvent.getFrameTime(); + frameTime = backEvent.getFrameTimeMillis(); } mIOnBackInvokedCallback.onBackStarted( new BackMotionEvent(backEvent.getTouchX(), backEvent.getTouchY(), frameTime, @@ -254,7 +254,7 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc try { long frameTime = 0; if (predictiveBackTimestampApi()) { - frameTime = backEvent.getFrameTime(); + frameTime = backEvent.getFrameTimeMillis(); } mIOnBackInvokedCallback.onBackProgressed( new BackMotionEvent(backEvent.getTouchX(), backEvent.getTouchY(), frameTime, diff --git a/core/res/Android.bp b/core/res/Android.bp index aa324fcaca58..f6ca8218926c 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -158,6 +158,7 @@ android_app { flags_packages: [ "android.app.appfunctions.flags-aconfig", "android.app.contextualsearch.flags-aconfig", + "android.appwidget.flags-aconfig", "android.content.pm.flags-aconfig", "android.provider.flags-aconfig", "camera_platform_flags", @@ -169,6 +170,7 @@ android_app { "android.media.tv.flags-aconfig", "android.security.flags-aconfig", "com.android.hardware.input.input-aconfig", + "aconfig_trade_in_mode_flags", ], } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 6ab64768d9f0..fb06e9630070 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1494,8 +1494,8 @@ android:description="@string/permdesc_readBasicPhoneState" android:protectionLevel="normal" /> - <!-- Allows read access to the device's phone number(s). This is a subset of the capabilities - granted by {@link #READ_PHONE_STATE} but is exposed to instant applications. + <!-- Allows read access to the device's phone number(s), + which is exposed to instant applications. <p>Protection level: dangerous--> <permission android:name="android.permission.READ_PHONE_NUMBERS" android:permissionGroup="android.permission-group.UNDEFINED" @@ -8464,6 +8464,14 @@ <permission android:name="android.permission.SETUP_FSVERITY" android:protectionLevel="signature|privileged"/> + <!-- Allows app to enter trade-in-mode. + <p>Protection level: signature|privileged + @hide + --> + <permission android:name="android.permission.ENTER_TRADE_IN_MODE" + android:protectionLevel="signature|privileged" + android:featureFlag="com.android.tradeinmode.flags.enable_trade_in_mode" /> + <!-- @TestApi Signature permission reserved for testing. This should never be used to diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 9854030ed0d1..b5892f6e1a77 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -460,4 +460,16 @@ <integer name="config_satellite_location_query_throttle_interval_minutes">10</integer> <java-symbol type="integer" name="config_satellite_location_query_throttle_interval_minutes" /> + <!-- Boolean indicating whether to enable MT SMS polling for NB IOT NTN. --> + <bool name="config_enabled_mt_sms_polling">true</bool> + <java-symbol type="bool" name="config_enabled_mt_sms_polling" /> + + <!-- Text to be used for MT SMS polling in NB IOT NTN. --> + <string name="config_mt_sms_polling_text" translatable="false">DU\\\#MMYSM€S2BIG\\\#NORED\\\!</string> + <java-symbol type="string" name="config_mt_sms_polling_text" /> + + <!-- The time duration in millis after which Telephony can send another MT SMS polling for NB IOT NTN --> + <integer name="config_mt_sms_polling_throttle_millis">300000</integer> + <java-symbol type="integer" name="config_mt_sms_polling_throttle_millis" /> + </resources> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 7184d9a8c890..522dcfaf4729 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -17,7 +17,7 @@ ** limitations under the License. */ --> -<resources> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> <!-- The width that is used when creating thumbnails of applications. --> <dimen name="thumbnail_width">192dp</dimen> <!-- The height that is used when creating thumbnails of applications. --> @@ -1037,9 +1037,12 @@ <dimen name="controls_thumbnail_image_max_width">280dp</dimen> <!-- System-provided radius for the background view of app widgets. The resolved value of this resource may change at runtime. --> - <dimen name="system_app_widget_background_radius">28dp</dimen> - <!-- System-provided radius for inner views on app widgets. The resolved value of this resource may change at runtime. --> - <dimen name="system_app_widget_inner_radius">20dp</dimen> + <dimen name="system_app_widget_background_radius" android:featureFlag="!android.appwidget.flags.use_smaller_app_widget_radius">28dp</dimen> + <dimen name="system_app_widget_background_radius" android:featureFlag="android.appwidget.flags.use_smaller_app_widget_radius">24dp</dimen> + <!-- System-provided radius for inner views on app widgets that are positioned 8dp within the widget background view. The resolved value of this resource may change at runtime. --> + <dimen name="system_app_widget_inner_radius" android:featureFlag="!android.appwidget.flags.use_smaller_app_widget_radius">20dp</dimen> + <!-- System-provided radius for inner views on app widgets that are positioned 8dp within the widget background view. The resolved value of this resource may change at runtime. --> + <dimen name="system_app_widget_inner_radius" android:featureFlag="android.appwidget.flags.use_smaller_app_widget_radius">16dp</dimen> <!-- System-provided padding for inner views on app widgets. The resolved value of this resource may change at runtime. @removed --> <dimen name="__removed_system_app_widget_internal_padding">16dp</dimen> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 581dee571a69..bb5380e1312d 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -34,7 +34,7 @@ http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ --> <!-- Arab Emirates --> - <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253" /> + <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253|6568" /> <!-- Albania: 5 digits, known short codes listed --> <shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" /> @@ -70,7 +70,7 @@ <shortcode country="bh" pattern="\\d{1,5}" free="81181|85999" /> <!-- Brazil: 1-5 digits (standard system default, not country specific) --> - <shortcode country="br" pattern="\\d{1,5}" free="6000[012]\\d|876|5500|9963|4141|8000|2652" /> + <shortcode country="br" pattern="\\d{1,5}" free="6000[012]\\d|876|5500|9963|4141|8000|2652|26808" /> <!-- Botswana: 1-5 digits (standard system default, not country specific) --> <shortcode country="bw" pattern="\\d{1,5}" free="16641" /> @@ -79,7 +79,7 @@ <shortcode country="by" pattern="\\d{4}" premium="3336|4161|444[4689]|501[34]|7781" /> <!-- Canada: 5-6 digits --> - <shortcode country="ca" pattern="\\d{5,6}" premium="60999|88188|43030" standard="244444" free="455677" /> + <shortcode country="ca" pattern="\\d{5,6}" premium="60999|88188|43030" standard="244444" free="455677|24470" /> <!-- Switzerland: 3-5 digits: http://www.swisscom.ch/fxres/kmu/thirdpartybusiness_code_of_conduct_en.pdf --> <shortcode country="ch" pattern="[2-9]\\d{2,4}" premium="543|83111|30118" free="98765|30075|30047" /> @@ -123,8 +123,8 @@ http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht --> <shortcode country="ee" pattern="1\\d{2,4}" premium="90\\d{5}|15330|1701[0-3]" free="116\\d{3}|95034" /> - <!-- Egypt: 4 digits, known codes listed --> - <shortcode country="eg" pattern="\\d{4}" free="1499" /> + <!-- Egypt: 4-5 digits, known codes listed --> + <shortcode country="eg" pattern="\\d{4,5}" free="1499|10020" /> <!-- Spain: 5-6 digits: 25xxx, 27xxx, 280xx, 35xxx, 37xxx, 795xxx, 797xxx, 995xxx, 997xxx, plus EU. http://www.legallink.es/?q=en/content/which-current-regulatory-status-premium-rate-services-spain --> @@ -147,7 +147,7 @@ <shortcode country="ge" pattern="\\d{1,5}" premium="801[234]|888[239]" free="95201|95202|95203" /> <!-- Ghana: 4 digits, known premium codes listed --> - <shortcode country="gh" pattern="\\d{4}" free="5041|3777|2333" /> + <shortcode country="gh" pattern="\\d{4}" free="5041|3777|2333|6061" /> <!-- Greece: 5 digits (54xxx, 19yxx, x=0-9, y=0-5): http://www.cmtelecom.com/premium-sms/greece --> <shortcode country="gr" pattern="\\d{5}" premium="54\\d{3}|19[0-5]\\d{2}" free="116\\d{3}|12115" /> @@ -169,7 +169,7 @@ <shortcode country="in" pattern="\\d{1,5}" free="59336|53969" /> <!-- Indonesia: 1-5 digits (standard system default, not country specific) --> - <shortcode country="id" pattern="\\d{1,5}" free="99477|6006|46645|363|93457|99265" /> + <shortcode country="id" pattern="\\d{1,5}" free="99477|6006|46645|363|93457|99265|77413" /> <!-- Ireland: 5 digits, 5xxxx (50xxx=free, 5[12]xxx=standard), plus EU: http://www.comreg.ie/_fileupload/publications/ComReg1117.pdf --> @@ -226,13 +226,13 @@ <shortcode country="mn" pattern="\\d{1,6}" free="44444|45678|445566" /> <!-- Malawi: 1-5 digits (standard system default, not country specific) --> - <shortcode country="mw" pattern="\\d{1,5}" free="4276" /> + <shortcode country="mw" pattern="\\d{1,5}" free="4276|4305" /> <!-- Mozambique: 1-5 digits (standard system default, not country specific) --> <shortcode country="mz" pattern="\\d{1,5}" free="1714" /> <!-- Mexico: 4-7 digits (not confirmed), known premium codes listed --> - <shortcode country="mx" pattern="\\d{4,7}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453|550346|3030303|81811" /> + <shortcode country="mx" pattern="\\d{4,7}" premium="53035|7766" free="26259|46645|50025|50052|5050|76551|88778|9963|91101|45453|550346|3030303|81811|81818" /> <!-- Malaysia: 5 digits: http://www.skmm.gov.my/attachment/Consumer_Regulation/Mobile_Content_Services_FAQs.pdf --> <shortcode country="my" pattern="\\d{5}" premium="32298|33776" free="22099|28288|66668|66966" /> @@ -324,7 +324,7 @@ <shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" /> <!-- Tanzania: 1-5 digits (standard system default, not country specific) --> - <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234|15324" /> + <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234|15324|15610" /> <!-- Tunisia: 5 digits, known premium codes listed --> <shortcode country="tn" pattern="\\d{5}" free="85799" /> @@ -336,11 +336,11 @@ <shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" /> <!-- Uganda(UG): 4 digits (standard system default, not country specific) --> - <shortcode country="ug" pattern="\\d{4}" free="8000" /> + <shortcode country="ug" pattern="\\d{4}" free="8000|8009" /> <!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm), visual voicemail code for T-Mobile: 122 --> - <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831" /> + <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831|10907" /> <!--Uruguay : 1-6 digits (standard system default, not country specific) --> <shortcode country="uy" pattern="\\d{1,6}" free="55002|191289" /> diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java index 46bd73e316f6..4d6c30ebbe2b 100644 --- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java +++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java @@ -686,7 +686,7 @@ public class WindowOnBackInvokedDispatcherTest { return new BackMotionEvent( /* touchX = */ 0, /* touchY = */ 0, - /* frameTime = */ 0, + /* frameTimeMillis = */ 0, /* progress = */ progress, /* triggerBack = */ false, /* swipeEdge = */ BackEvent.EDGE_LEFT, diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index a028e1829ac6..debd0df95cdf 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -659,5 +659,6 @@ applications that come with the platform <privapp-permissions package="com.android.devicediagnostics"> <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/> <permission name="android.permission.BATTERY_STATS"/> + <permission name="android.permission.ENTER_TRADE_IN_MODE"/> </privapp-permissions> </permissions> diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java index cb3b64c3e6cd..93d94c9cd7eb 100644 --- a/graphics/java/android/graphics/ImageFormat.java +++ b/graphics/java/android/graphics/ImageFormat.java @@ -16,6 +16,7 @@ package android.graphics; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import java.lang.annotation.Retention; @@ -41,6 +42,7 @@ public class ImageFormat { Y8, Y16, YCBCR_P010, + YCBCR_P210, NV16, NV21, YUY2, @@ -206,6 +208,26 @@ public class ImageFormat { public static final int YCBCR_P010 = 0x36; /** + * <p>Android YUV P210 format.</p> + * + * P210 is a 4:2:2 YCbCr semiplanar format comprised of a WxH Y plane + * followed by a WxH CbCr plane. Each sample is represented by a 16-bit + * little-endian value, with the lower 6 bits set to zero. + * + * <p>For example, the {@link android.media.Image} object can provide data + * in this format from a {@link android.hardware.camera2.CameraDevice} + * through a {@link android.media.ImageReader} object if this format is + * supported by {@link android.hardware.camera2.CameraDevice}.</p> + * + * @see android.media.Image + * @see android.media.ImageReader + * @see android.hardware.camera2.CameraDevice + * + */ + @FlaggedApi(android.media.codec.Flags.FLAG_P210_FORMAT_SUPPORT) + public static final int YCBCR_P210 = 0x3c; + + /** * YCbCr format, used for video. * * <p>For the {@link android.hardware.camera2} API, the {@link #YUV_420_888} format is @@ -849,6 +871,8 @@ public class ImageFormat { return 16; case YCBCR_P010: return 24; + case YCBCR_P210: + return 32; case RAW_DEPTH10: case RAW10: return 10; @@ -899,7 +923,9 @@ public class ImageFormat { case JPEG_R: return true; } - + if (android.media.codec.Flags.p210FormatSupport() && format == YCBCR_P210) { + return true; + } return false; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java index 7a569799ab84..dc50fdbd1af3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -16,12 +16,15 @@ package com.android.wm.shell.back; +import static android.view.MotionEvent.ACTION_MOVE; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.window.BackEvent.EDGE_RIGHT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_TASK; +import static com.android.window.flags.Flags.predictiveBackTimestampApi; import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD; +import static com.android.wm.shell.back.CrossActivityBackAnimationKt.scaleCentered; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; import android.animation.Animator; @@ -36,11 +39,14 @@ import android.graphics.Rect; import android.graphics.RectF; import android.os.Handler; import android.os.RemoteException; +import android.util.TimeUtils; import android.view.Choreographer; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; +import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.view.VelocityTracker; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.window.BackEvent; @@ -48,6 +54,9 @@ import android.window.BackMotionEvent; import android.window.BackProgressAnimator; import android.window.IOnBackInvokedCallback; +import com.android.internal.dynamicanimation.animation.FloatValueHolder; +import com.android.internal.dynamicanimation.animation.SpringAnimation; +import com.android.internal.dynamicanimation.animation.SpringForce; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.SystemBarUtils; import com.android.internal.protolog.ProtoLog; @@ -81,6 +90,11 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { /** Duration of post animation after gesture committed. */ private static final int POST_ANIMATION_DURATION_MS = 500; + private static final float SPRING_SCALE = 100f; + private static final float DEFAULT_FLING_VELOCITY = 320f; + private static final float MAX_FLING_VELOCITY = 1000f; + private static final float FLING_SPRING_STIFFNESS = 320f; + private final Rect mStartTaskRect = new Rect(); private float mCornerRadius; private int mStatusbarHeight; @@ -114,6 +128,14 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { private float mInterWindowMargin; private float mVerticalMargin; + private final FloatValueHolder mPostCommitFlingScale = new FloatValueHolder(SPRING_SCALE); + private final SpringForce mPostCommitFlingSpring = new SpringForce(SPRING_SCALE) + .setStiffness(FLING_SPRING_STIFFNESS) + .setDampingRatio(1f); + private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + private float mGestureProgress = 0f; + private long mDownTime = 0L; + @Inject public CrossTaskBackAnimation(Context context, BackAnimationBackground background, @ShellMainThread Handler handler) { @@ -168,6 +190,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { if (mEnteringTarget == null || mClosingTarget == null) { return; } + mGestureProgress = progress; float touchY = event.getTouchY(); @@ -229,6 +252,8 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { } mClosingCurrentRect.set(left, top, left + width, top + height); + + applyFlingScale(mClosingCurrentRect); applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius); } @@ -239,9 +264,19 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height()); mEnteringCurrentRect.set(left, top, left + width, top + height); + + applyFlingScale(mEnteringCurrentRect); applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius); } + private void applyFlingScale(RectF rect) { + // apply a scale to the rect to account for fling velocity + final float flingScale = Math.min(mPostCommitFlingScale.getValue() / SPRING_SCALE, 1f); + if (flingScale >= 1f) return; + scaleCentered(rect, flingScale, /* pivotX */ rect.right, + /* pivotY */ rect.top + rect.height() / 2); + } + /** Transform the target window to match the target rect. */ private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) { if (leash == null || !leash.isValid()) { @@ -280,6 +315,9 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { mTransformMatrix.reset(); mClosingCurrentRect.setEmpty(); mInitialTouchPos.set(0, 0); + mGestureProgress = 0; + mDownTime = 0; + mVelocityTracker.clear(); if (mFinishCallback != null) { try { @@ -295,10 +333,24 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { private void onGestureProgress(@NonNull BackEvent backEvent) { if (!mBackInProgress) { mBackInProgress = true; + mDownTime = backEvent.getFrameTimeMillis(); } float progress = backEvent.getProgress(); mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); - updateGestureBackProgress(getInterpolatedProgress(progress), backEvent); + float interpolatedProgress = getInterpolatedProgress(progress); + if (predictiveBackTimestampApi()) { + mVelocityTracker.addMovement( + MotionEvent.obtain( + /* downTime */ mDownTime, + /* eventTime */ backEvent.getFrameTimeMillis(), + /* action */ ACTION_MOVE, + /* x */ interpolatedProgress * SPRING_SCALE, + /* y */ 0f, + /* metaState */ 0 + ) + ); + } + updateGestureBackProgress(interpolatedProgress, backEvent); } private void onGestureCommitted() { @@ -307,6 +359,25 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { return; } + if (predictiveBackTimestampApi()) { + // kick off spring animation with the current velocity from the pre-commit phase, this + // affects the scaling of the closing and/or opening task during post-commit + mVelocityTracker.computeCurrentVelocity(1000); + float startVelocity = mGestureProgress < 0.1f + ? -DEFAULT_FLING_VELOCITY : -mVelocityTracker.getXVelocity(); + SpringAnimation flingAnimation = + new SpringAnimation(mPostCommitFlingScale, SPRING_SCALE) + .setStartVelocity(Math.max(-MAX_FLING_VELOCITY, Math.min(0f, startVelocity))) + .setStartValue(SPRING_SCALE) + .setMinimumVisibleChange(0.1f) + .setSpring(mPostCommitFlingSpring); + flingAnimation.start(); + // do an animation-frame immediately to prevent idle frame + flingAnimation.doAnimationFrame( + Choreographer.getInstance().getLastFrameTimeNanos() / TimeUtils.NANOS_PER_MS + ); + } + // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current // coordinate of the gesture driven phase. mEnteringCurrentRect.round(mEnteringStartRect); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java index 915a8a149d54..37369d1f3047 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java @@ -24,6 +24,7 @@ import java.util.function.IntSupplier; /** Handle the visibility state of the Compat UI components. */ public class CompatUIStatusManager { + private static final int COMPAT_UI_EDUCATION_UNDEFINED = -1; public static final int COMPAT_UI_EDUCATION_HIDDEN = 0; public static final int COMPAT_UI_EDUCATION_VISIBLE = 1; @@ -32,24 +33,40 @@ public class CompatUIStatusManager { @NonNull private final IntSupplier mReader; + private int mCurrentValue = COMPAT_UI_EDUCATION_UNDEFINED; + public CompatUIStatusManager(@NonNull IntConsumer writer, @NonNull IntSupplier reader) { mWriter = writer; mReader = reader; } public CompatUIStatusManager() { - this(i -> { }, () -> COMPAT_UI_EDUCATION_HIDDEN); + this(i -> { + }, () -> COMPAT_UI_EDUCATION_HIDDEN); } void onEducationShown() { - mWriter.accept(COMPAT_UI_EDUCATION_VISIBLE); + if (mCurrentValue != COMPAT_UI_EDUCATION_VISIBLE) { + mCurrentValue = COMPAT_UI_EDUCATION_VISIBLE; + mWriter.accept(mCurrentValue); + } } void onEducationHidden() { - mWriter.accept(COMPAT_UI_EDUCATION_HIDDEN); + if (mCurrentValue != COMPAT_UI_EDUCATION_HIDDEN) { + mCurrentValue = COMPAT_UI_EDUCATION_HIDDEN; + mWriter.accept(mCurrentValue); + } } boolean isEducationVisible() { - return mReader.getAsInt() == COMPAT_UI_EDUCATION_VISIBLE; + return getCurrentValue() == COMPAT_UI_EDUCATION_VISIBLE; + } + + private int getCurrentValue() { + if (mCurrentValue == COMPAT_UI_EDUCATION_UNDEFINED) { + mCurrentValue = mReader.getAsInt(); + } + return mCurrentValue; } -} +}
\ No newline at end of file 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 4c2588984500..b700a5455f1a 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 @@ -67,7 +67,7 @@ import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; -import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler; +import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; @@ -397,12 +397,12 @@ public abstract class WMShellModule { Context context, ShellInit shellInit, Transitions transitions, - Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler, + Optional<DesktopImmersiveController> desktopImmersiveController, WindowDecorViewModel windowDecorViewModel, Optional<TaskChangeListener> taskChangeListener, FocusTransitionObserver focusTransitionObserver) { return new FreeformTaskTransitionObserver( - context, shellInit, transitions, desktopImmersiveTransitionHandler, + context, shellInit, transitions, desktopImmersiveController, windowDecorViewModel, taskChangeListener, focusTransitionObserver); } @@ -638,7 +638,7 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopRepository desktopRepository, - Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler, + Optional<DesktopImmersiveController> desktopImmersiveController, DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, @@ -657,7 +657,7 @@ public abstract class WMShellModule { returnToDragStartAnimator, enterDesktopTransitionHandler, exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler, toggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(), + dragToDesktopTransitionHandler, desktopImmersiveController.get(), desktopRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter, @@ -705,7 +705,7 @@ public abstract class WMShellModule { @WMSingleton @Provides - static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler( + static Optional<DesktopImmersiveController> provideDesktopImmersiveController( Context context, Transitions transitions, @DynamicOverride DesktopRepository desktopRepository, @@ -713,7 +713,7 @@ public abstract class WMShellModule { ShellTaskOrganizer shellTaskOrganizer) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of( - new DesktopFullImmersiveTransitionHandler( + new DesktopImmersiveController( transitions, desktopRepository, displayController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt index 9d4926b47def..d0bc5f0955f7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt @@ -36,20 +36,21 @@ import com.android.wm.shell.common.DisplayController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionHandler +import com.android.wm.shell.transition.Transitions.TransitionObserver import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener /** - * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task + * A controller to move tasks in/out of desktop's full immersive state where the task * remains freeform while being able to take fullscreen bounds and have its App Header visibility * be transient below the status bar like in fullscreen immersive mode. */ -class DesktopFullImmersiveTransitionHandler( +class DesktopImmersiveController( private val transitions: Transitions, private val desktopRepository: DesktopRepository, private val displayController: DisplayController, private val shellTaskOrganizer: ShellTaskOrganizer, private val transactionSupplier: () -> SurfaceControl.Transaction, -) : TransitionHandler { +) : TransitionHandler, TransitionObserver { constructor( transitions: Transitions, @@ -67,7 +68,7 @@ class DesktopFullImmersiveTransitionHandler( private var state: TransitionState? = null @VisibleForTesting - val pendingExternalExitTransitions = mutableSetOf<ExternalPendingExit>() + val pendingExternalExitTransitions = mutableListOf<ExternalPendingExit>() /** Whether there is an immersive transition that hasn't completed yet. */ private val inProgress: Boolean @@ -137,14 +138,19 @@ class DesktopFullImmersiveTransitionHandler( * * @param wct that will apply these changes * @param displayId of the display that should exit immersive mode + * @param excludeTaskId of the task to ignore (not exit) if it is the immersive one * @return a function to apply once the transition that will apply these changes is started */ fun exitImmersiveIfApplicable( wct: WindowContainerTransaction, - displayId: Int + displayId: Int, + excludeTaskId: Int? = null, ): ((IBinder) -> Unit)? { if (!Flags.enableFullyImmersiveInDesktop()) return null val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) ?: return null + if (immersiveTask == excludeTaskId) { + return null + } val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) ?: return null logV("Appending immersive exit for task: $immersiveTask in display: $displayId") wct.setBounds(taskInfo.token, getExitDestinationBounds(taskInfo)) @@ -179,6 +185,17 @@ class DesktopFullImmersiveTransitionHandler( return null } + + /** Whether the [change] in the [transition] is a known immersive change. */ + fun isImmersiveChange( + transition: IBinder, + change: TransitionInfo.Change, + ): Boolean { + return pendingExternalExitTransitions.any { + it.transition == transition && it.taskId == change.taskInfo?.taskId + } + } + private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) { pendingExternalExitTransitions.add( ExternalPendingExit( @@ -196,10 +213,11 @@ class DesktopFullImmersiveTransitionHandler( finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ): Boolean { + logD("startAnimation transition=%s", transition) val state = requireState() if (transition != state.transition) return false animateResize( - transitionState = state, + targetTaskId = state.taskId, info = info, startTransaction = startTransaction, finishTransaction = finishTransaction, @@ -209,40 +227,55 @@ class DesktopFullImmersiveTransitionHandler( } private fun animateResize( - transitionState: TransitionState, + targetTaskId: Int, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ) { + logD("animateResize for task#%d", targetTaskId) val change = info.changes.first { c -> val taskInfo = c.taskInfo - return@first taskInfo != null && taskInfo.taskId == transitionState.taskId + return@first taskInfo != null && taskInfo.taskId == targetTaskId } + animateResizeChange(change, startTransaction, finishTransaction, finishCallback) + } + + /** + * Animate an immersive change. + * + * As of now, both enter and exit transitions have the same animation, a veiled resize. + */ + fun animateResizeChange( + change: TransitionInfo.Change, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ) { + val taskId = change.taskInfo!!.taskId val leash = change.leash val startBounds = change.startAbsBounds val endBounds = change.endAbsBounds - + logD("Animating resize change for task#%d from %s to %s", taskId, startBounds, endBounds) + + startTransaction + .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat()) + .setWindowCrop(leash, startBounds.width(), startBounds.height()) + .show(leash) + onTaskResizeAnimationListener + ?.onAnimationStart(taskId, startTransaction, startBounds) + ?: startTransaction.apply() val updateTransaction = transactionSupplier() ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply { duration = FULL_IMMERSIVE_ANIM_DURATION_MS interpolator = DecelerateInterpolator() addListener( - onStart = { - startTransaction - .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat()) - .setWindowCrop(leash, startBounds.width(), startBounds.height()) - .show(leash) - onTaskResizeAnimationListener - ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds) - ?: startTransaction.apply() - }, onEnd = { finishTransaction .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat()) .setWindowCrop(leash, endBounds.width(), endBounds.height()) .apply() - onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId) + onTaskResizeAnimationListener?.onAnimationEnd(taskId) finishCallback.onTransitionFinished(null /* wct */) clearState() } @@ -254,7 +287,7 @@ class DesktopFullImmersiveTransitionHandler( .setWindowCrop(leash, rect.width(), rect.height()) .apply() onTaskResizeAnimationListener - ?.onBoundsChange(transitionState.taskId, updateTransaction, rect) + ?.onBoundsChange(taskId, updateTransaction, rect) ?: updateTransaction.apply() } start() @@ -284,15 +317,20 @@ class DesktopFullImmersiveTransitionHandler( * |onTransitionReady|, before this transition actually animates) because drawing decorations * depends on whether the task is in full immersive state or not. */ - fun onTransitionReady(transition: IBinder, info: TransitionInfo) { + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + ) { + logD("onTransitionReady transition=%s", transition) // Check if this is a pending external exit transition. val pendingExit = pendingExternalExitTransitions .firstOrNull { pendingExit -> pendingExit.transition == transition } if (pendingExit != null) { - pendingExternalExitTransitions.remove(pendingExit) if (info.hasTaskChange(taskId = pendingExit.taskId)) { if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) { - logV("Pending external exit for task ${pendingExit.taskId} verified") + logV("Pending external exit for task#%d verified", pendingExit.taskId) desktopRepository.setTaskInFullImmersiveState( displayId = pendingExit.displayId, taskId = pendingExit.taskId, @@ -311,7 +349,7 @@ class DesktopFullImmersiveTransitionHandler( val state = requireState() val startBounds = info.changes.first { c -> c.taskInfo?.taskId == state.taskId } .startAbsBounds - logV("Direct move for task ${state.taskId} in ${state.direction} direction verified") + logV("Direct move for task#%d in %s direction verified", state.taskId, state.direction) when (state.direction) { Direction.ENTER -> { desktopRepository.setTaskInFullImmersiveState( @@ -343,7 +381,7 @@ class DesktopFullImmersiveTransitionHandler( .filter { c -> desktopRepository.isTaskInFullImmersiveState(c.taskInfo!!.taskId) } .filter { c -> c.startRotation != c.endRotation } .forEach { c -> - logV("Detected immersive exit due to rotation for task: ${c.taskInfo!!.taskId}") + logV("Detected immersive exit due to rotation for task#%d", c.taskInfo!!.taskId) desktopRepository.setTaskInFullImmersiveState( displayId = c.taskInfo!!.displayId, taskId = c.taskInfo!!.taskId, @@ -352,6 +390,32 @@ class DesktopFullImmersiveTransitionHandler( } } + override fun onTransitionMerged(merged: IBinder, playing: IBinder) { + logD("onTransitionMerged merged=%s playing=%s", merged, playing) + val pendingExit = pendingExternalExitTransitions + .firstOrNull { pendingExit -> pendingExit.transition == merged } + if (pendingExit != null) { + logV( + "Pending exit transition %s for task#%s merged into %s", + merged, pendingExit.taskId, playing + ) + pendingExit.transition = playing + } + } + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + logD("onTransitionFinished transition=%s aborted=%b", transition, aborted) + val pendingExit = pendingExternalExitTransitions + .firstOrNull { pendingExit -> pendingExit.transition == transition } + if (pendingExit != null) { + logV( + "Pending exit transition %s for task#%s finished", + transition, pendingExit + ) + pendingExternalExitTransitions.remove(pendingExit) + } + } + private fun clearState() { state = null } @@ -394,7 +458,7 @@ class DesktopFullImmersiveTransitionHandler( data class ExternalPendingExit( val taskId: Int, val displayId: Int, - val transition: IBinder, + var transition: IBinder, ) private enum class Direction { @@ -405,6 +469,10 @@ class DesktopFullImmersiveTransitionHandler( ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + private companion object { private const val TAG = "DesktopImmersive" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index df9fc59b925e..dba46d0c234a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -138,7 +138,7 @@ class DesktopMixedTransitionHandler( private fun isLastDesktopTask(change: TransitionInfo.Change): Boolean = change.taskInfo?.let { - desktopRepository.getActiveNonMinimizedTaskCount(it.displayId) == 1 + desktopRepository.getExpandedTaskCount(it.displayId) == 1 } ?: false private fun findCloseDesktopTaskChange(info: TransitionInfo): TransitionInfo.Change? { 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 85a3126d9de6..5998dc848e2b 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 @@ -260,11 +260,11 @@ class DesktopRepository ( ArraySet(desktopTaskDataByDisplayId[displayId]?.minimizedTasks) /** Returns all active non-minimized tasks for [displayId] ordered from top to bottom. */ - fun getActiveNonMinimizedOrderedTasks(displayId: Int): List<Int> = + fun getExpandedTasksOrdered(displayId: Int): List<Int> = getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) } /** Returns the count of active non-minimized tasks for [displayId]. */ - fun getActiveNonMinimizedTaskCount(displayId: Int): Int { + fun getExpandedTaskCount(displayId: Int): Int { return getActiveTasks(displayId).count { !isMinimizedTask(it) } } 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 69776cd4740a..77fb4b4815f1 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 @@ -48,6 +48,7 @@ import android.view.DragEvent import android.view.KeyEvent import android.view.MotionEvent import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_NONE @@ -59,6 +60,7 @@ import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVI import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS import android.window.RemoteTransition import android.window.TransitionInfo +import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.annotation.BinderThread @@ -115,6 +117,7 @@ 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.transition.Transitions.TransitionFinishCallback import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener @@ -146,7 +149,7 @@ class DesktopTasksController( private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler, private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, - private val immersiveTransitionHandler: DesktopFullImmersiveTransitionHandler, + private val desktopImmersiveController: DesktopImmersiveController, private val taskRepository: DesktopRepository, private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver, private val launchAdjacentController: LaunchAdjacentController, @@ -252,7 +255,7 @@ class DesktopTasksController( toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener - immersiveTransitionHandler.onTaskResizeAnimationListener = listener + desktopImmersiveController.onTaskResizeAnimationListener = listener } fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { @@ -372,8 +375,11 @@ class DesktopTasksController( // TODO(342378842): Instead of using default display, support multiple displays val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask( DEFAULT_DISPLAY, wct, taskId) - val runOnTransit = immersiveTransitionHandler - .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY) + val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + wct = wct, + displayId = DEFAULT_DISPLAY, + excludeTaskId = taskId, + ) wct.startTask( taskId, ActivityOptions.makeBasic().apply { @@ -400,7 +406,11 @@ class DesktopTasksController( } logV("moveRunningTaskToDesktop taskId=%d", task.taskId) exitSplitIfApplicable(wct, task) - val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, task.displayId) + val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + wct = wct, + displayId = task.displayId, + excludeTaskId = task.taskId, + ) // Bring other apps to front first val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) @@ -445,7 +455,7 @@ class DesktopTasksController( val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId) addMoveToDesktopChanges(wct, taskInfo) - val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable( + val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( wct, taskInfo.displayId) val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) transition?.let { @@ -492,7 +502,7 @@ class DesktopTasksController( taskId ) ) - return immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo) + return desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo) } fun minimizeTask(taskInfo: RunningTaskInfo) { @@ -505,7 +515,7 @@ class DesktopTasksController( removeWallpaperActivity(wct) } // Notify immersive handler as it might need to exit immersive state. - val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo) + val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo) wct.reorder(taskInfo.token, false) val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct) @@ -609,8 +619,11 @@ class DesktopTasksController( logV("moveBackgroundTaskToFront taskId=%s", taskId) val wct = WindowContainerTransaction() // TODO: b/342378842 - Instead of using default display, support multiple displays - val runOnTransit = immersiveTransitionHandler - .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY) + val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + wct = wct, + displayId = DEFAULT_DISPLAY, + excludeTaskId = taskId, + ) wct.startTask( taskId, ActivityOptions.makeBasic().apply { @@ -632,8 +645,11 @@ class DesktopTasksController( logV("moveTaskToFront taskId=%s", taskInfo.taskId) val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */) - val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable( - wct, taskInfo.displayId) + val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + wct = wct, + displayId = taskInfo.displayId, + excludeTaskId = taskInfo.taskId, + ) val transition = startLaunchTransition(TRANSIT_TO_FRONT, wct, taskInfo.taskId, remoteTransition) runOnTransit?.invoke(transition) @@ -646,7 +662,7 @@ class DesktopTasksController( remoteTransition: RemoteTransition?, ): IBinder { val taskToMinimize: RunningTaskInfo? = - addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId) + addAndGetMinimizeChanges(DEFAULT_DISPLAY, wct, taskId) if (remoteTransition == null) { val t = transitions.startTransition(transitionType, wct, null /* handler */) addPendingMinimizeTransition(t, taskToMinimize) @@ -736,12 +752,12 @@ class DesktopTasksController( private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) { check(taskInfo.isFreeform) { "Task must already be in freeform" } - immersiveTransitionHandler.moveTaskToImmersive(taskInfo) + desktopImmersiveController.moveTaskToImmersive(taskInfo) } private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) { check(taskInfo.isFreeform) { "Task must already be in freeform" } - immersiveTransitionHandler.moveTaskToNonImmersive(taskInfo) + desktopImmersiveController.moveTaskToNonImmersive(taskInfo) } /** @@ -856,7 +872,7 @@ class DesktopTasksController( excludeTaskId: Int? = null, ): Boolean { val doesAnyTaskRequireTaskbarRounding = - taskRepository.getActiveNonMinimizedOrderedTasks(displayId) + taskRepository.getExpandedTasksOrdered(displayId) // exclude current task since maximize/restore transition has not taken place yet. .filterNot { taskId -> taskId == excludeTaskId } .any { taskId -> @@ -1034,23 +1050,23 @@ class DesktopTasksController( addWallpaperActivity(wct) } - val nonMinimizedTasksOrderedFrontToBack = - taskRepository.getActiveNonMinimizedOrderedTasks(displayId) + val expandedTasksOrderedFrontToBack = + taskRepository.getExpandedTasksOrdered(displayId) // If we're adding a new Task we might need to minimize an old one // TODO(b/365725441): Handle non running task minimization val taskToMinimize: RunningTaskInfo? = if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { desktopTasksLimiter .get() - .getTaskToMinimizeIfNeeded( - nonMinimizedTasksOrderedFrontToBack, + .getTaskToMinimize( + expandedTasksOrderedFrontToBack, newTaskIdInFront ) } else { null } - nonMinimizedTasksOrderedFrontToBack + expandedTasksOrderedFrontToBack // If there is a Task to minimize, let it stay behind the Home Task .filter { taskId -> taskId != taskToMinimize?.taskId } .reversed() // Start from the back so the front task is brought forward last @@ -1220,6 +1236,67 @@ class DesktopTasksController( return result } + /** Whether the given [change] in the [transition] is a known desktop change. */ + fun isDesktopChange( + transition: IBinder, + change: TransitionInfo.Change, + ): Boolean { + // Only the immersive controller is currently involved in mixed transitions. + return Flags.enableFullyImmersiveInDesktop() + && desktopImmersiveController.isImmersiveChange(transition, change) + } + + /** + * Whether the given transition [info] will potentially include a desktop change, in which + * case the transition should be treated as mixed so that the change is in part animated by + * one of the desktop transition handlers. + */ + fun shouldPlayDesktopAnimation(info: TransitionRequestInfo): Boolean { + // Only immersive mixed transition are currently supported. + if (!Flags.enableFullyImmersiveInDesktop()) return false + val triggerTask = info.triggerTask ?: return false + if (!isDesktopModeShowing(triggerTask.displayId)) { + return false + } + if (!TransitionUtil.isOpeningType(info.type)) { + return false + } + taskRepository.getTaskInFullImmersiveState(displayId = triggerTask.displayId) + ?: return false + return when { + triggerTask.isFullscreen -> { + // Trigger fullscreen task will enter desktop, so any existing immersive task + // should exit. + shouldFullscreenTaskLaunchSwitchToDesktop(triggerTask) + } + triggerTask.isFreeform -> { + // Trigger freeform task will enter desktop, so any existing immersive task should + // exit. + !shouldFreeformTaskLaunchSwitchToFullscreen(triggerTask) + } + else -> false + } + } + + /** Animate a desktop change found in a mixed transitions. */ + fun animateDesktopChange( + transition: IBinder, + change: Change, + startTransaction: Transaction, + finishTransaction: Transaction, + finishCallback: TransitionFinishCallback, + ) { + if (!desktopImmersiveController.isImmersiveChange(transition, change)) { + throw IllegalStateException("Only immersive changes support desktop mixed transitions") + } + desktopImmersiveController.animateResizeChange( + change, + startTransaction, + finishTransaction, + finishCallback + ) + } + private fun taskContainsDragAndDropCookie(taskInfo: RunningTaskInfo?) = taskInfo?.launchCookies?.any { it == dragAndDropFullscreenCookie } ?: false @@ -1266,8 +1343,11 @@ class DesktopTasksController( wct.startTask(requestedTaskId, options.toBundle()) val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask( callingTask.displayId, wct, requestedTaskId) - val runOnTransit = immersiveTransitionHandler - .exitImmersiveIfApplicable(wct, callingTask.displayId) + val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + wct = wct, + displayId = callingTask.displayId, + excludeTaskId = requestedTaskId, + ) val transition = transitions.startTransition(TRANSIT_OPEN, wct, null) addPendingMinimizeTransition(transition, taskToMinimize) runOnTransit?.invoke(transition) @@ -1376,7 +1456,7 @@ class DesktopTasksController( return null } val wct = WindowContainerTransaction() - if (!isDesktopModeShowing(task.displayId)) { + if (shouldFreeformTaskLaunchSwitchToFullscreen(task)) { logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId) if (taskRepository.isActiveTask(task.taskId) && !forceEnterDesktop(task.displayId)) { // We are outside of desktop mode and already existing desktop task is being @@ -1407,9 +1487,9 @@ class DesktopTasksController( } // Desktop Mode is showing and we're launching a new Task: // 1) Exit immersive if needed. - immersiveTransitionHandler.exitImmersiveIfApplicable(transition, wct, task.displayId) + desktopImmersiveController.exitImmersiveIfApplicable(transition, wct, task.displayId) // 2) minimize a Task if needed. - val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId) + val taskToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId) if (taskToMinimize != null) { addPendingMinimizeTransition(transition, taskToMinimize) return wct @@ -1422,7 +1502,7 @@ class DesktopTasksController( transition: IBinder ): WindowContainerTransaction? { logV("handleFullscreenTaskLaunch") - if (isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)) { + if (shouldFullscreenTaskLaunchSwitchToDesktop(task)) { logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId) return WindowContainerTransaction().also { wct -> addMoveToDesktopChanges(wct, task) @@ -1436,9 +1516,9 @@ class DesktopTasksController( // Desktop Mode is already showing and we're launching a new Task - we might need to // minimize another Task. val taskToMinimize = - addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId) + addAndGetMinimizeChanges(task.displayId, wct, task.taskId) addPendingMinimizeTransition(transition, taskToMinimize) - immersiveTransitionHandler.exitImmersiveIfApplicable( + desktopImmersiveController.exitImmersiveIfApplicable( transition, wct, task.displayId ) } @@ -1446,6 +1526,12 @@ class DesktopTasksController( return null } + private fun shouldFreeformTaskLaunchSwitchToFullscreen(task: RunningTaskInfo): Boolean = + !isDesktopModeShowing(task.displayId) + + private fun shouldFullscreenTaskLaunchSwitchToDesktop(task: RunningTaskInfo): Boolean = + isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId) + /** * If a task is not compatible with desktop mode freeform, it should always be launched in * fullscreen. @@ -1564,7 +1650,7 @@ class DesktopTasksController( val stableBounds = Rect() displayLayout.getStableBoundsForDesktopMode(stableBounds) - val activeTasks = taskRepository.getActiveNonMinimizedOrderedTasks(displayId) + val activeTasks = taskRepository.getExpandedTasksOrdered(displayId) activeTasks.firstOrNull()?.let { activeTask -> shellTaskOrganizer.getRunningTaskInfo(activeTask)?.let { cascadeWindow(context.resources, stableBounds, @@ -1593,7 +1679,7 @@ class DesktopTasksController( } /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ - private fun addAndGetMinimizeChangesIfNeeded( + private fun addAndGetMinimizeChanges( displayId: Int, wct: WindowContainerTransaction, newTaskId: Int @@ -1601,7 +1687,7 @@ class DesktopTasksController( if (!desktopTasksLimiter.isPresent) return null return desktopTasksLimiter .get() - .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskId) + .addAndGetMinimizeTaskChanges(displayId, wct, newTaskId) } private fun addPendingMinimizeTransition( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index d6b721253abf..cd28a4fa67ec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -30,7 +30,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW import com.android.internal.jank.InteractionJankMonitor import com.android.internal.protolog.ProtoLog import com.android.wm.shell.ShellTaskOrganizer -import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionObserver @@ -57,13 +57,11 @@ class DesktopTasksLimiter ( init { require(maxTasksLimit > 0) { - "DesktopTasksLimiter should not be created with a maxTasksLimit at 0 or less. " + - "Current value: $maxTasksLimit." + "DesktopTasksLimiter: maxTasksLimit should be greater than 0. Current value: $maxTasksLimit." } transitions.registerObserver(minimizeTransitionObserver) taskRepository.addActiveTaskListener(leftoverMinimizedTasksRemover) - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: starting limiter with a maximum of %d tasks", maxTasksLimit) + logV("Starting limiter with a maximum of %d tasks", maxTasksLimit) } private data class TaskDetails( @@ -88,20 +86,14 @@ class DesktopTasksLimiter ( finishTransaction: SurfaceControl.Transaction ) { val taskToMinimize = pendingTransitionTokensAndTasks.remove(transition) ?: return - if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return - - if (!isTaskReorderedToBackOrInvisible(info, taskToMinimize)) { - ProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: task %d is not reordered to back nor invis", - taskToMinimize.taskId) + if (!isTaskReadyForMinimize(info, taskToMinimize)) { + logV("task %d is not reordered to back nor invis", taskToMinimize.taskId) return } - taskToMinimize.transitionInfo = info activeTransitionTokensAndTasks[transition] = taskToMinimize - this@DesktopTasksLimiter.markTaskMinimized( + this@DesktopTasksLimiter.minimizeTask( taskToMinimize.displayId, taskToMinimize.taskId) } @@ -109,18 +101,15 @@ class DesktopTasksLimiter ( * Returns whether the Task [taskDetails] is being reordered to the back in the transition * [info], or is already invisible. * - * This check can be used to double-check that a task was indeed minimized before - * marking it as such. + * This check confirms a task should be minimized before minimizing it. */ - private fun isTaskReorderedToBackOrInvisible( - info: TransitionInfo, - taskDetails: TaskDetails + private fun isTaskReadyForMinimize( + info: TransitionInfo, + taskDetails: TaskDetails ): Boolean { val taskChange = info.changes.find { change -> change.taskInfo?.taskId == taskDetails.taskId } - if (taskChange == null) { - return !taskRepository.isVisibleTask(taskDetails.taskId) - } + if (taskChange == null) return !taskRepository.isVisibleTask(taskDetails.taskId) return taskChange.mode == TRANSIT_TO_BACK } @@ -145,9 +134,7 @@ class DesktopTasksLimiter ( } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { - ProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: transition %s finished", transition) + logV("transition %s finished", transition) if (activeTransitionTokensAndTasks.remove(transition) != null) { if (aborted) { interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) @@ -170,18 +157,11 @@ class DesktopTasksLimiter ( } fun removeLeftoverMinimizedTasks(displayId: Int, wct: WindowContainerTransaction) { - if (taskRepository.getActiveNonMinimizedOrderedTasks(displayId).isNotEmpty()) { - return - } + if (taskRepository.getExpandedTasksOrdered(displayId).isNotEmpty()) return val remainingMinimizedTasks = taskRepository.getMinimizedTasks(displayId) - if (remainingMinimizedTasks.isEmpty()) { - return - } - ProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: removing leftover minimized tasks: %s", - remainingMinimizedTasks, - ) + if (remainingMinimizedTasks.isEmpty()) return + + logV("Removing leftover minimized tasks: %s", remainingMinimizedTasks) remainingMinimizedTasks.forEach { taskIdToRemove -> val taskToRemove = shellTaskOrganizer.getRunningTaskInfo(taskIdToRemove) if (taskToRemove != null) { @@ -192,35 +172,30 @@ class DesktopTasksLimiter ( } /** - * Mark [taskId], which must be on [displayId], as minimized, this should only be done after the - * corresponding transition has finished so we don't minimize the task if the transition fails. + * Mark task with [taskId] on [displayId] as minimized. + * + * This should be after the corresponding transition has finished so we don't + * minimize the task if the transition fails. */ - private fun markTaskMinimized(displayId: Int, taskId: Int) { - ProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: marking %d as minimized", taskId) + private fun minimizeTask(displayId: Int, taskId: Int) { + logV("Minimize taskId=%d, displayId=%d", taskId, displayId) taskRepository.minimizeTask(displayId, taskId) } /** - * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task + * Adds a minimize-transition to [wct] if adding [newFrontTaskInfo] crosses task * limit, returning the task to minimize. - * - * The task must be on [displayId]. */ - fun addAndGetMinimizeTaskChangesIfNeeded( + fun addAndGetMinimizeTaskChanges( displayId: Int, wct: WindowContainerTransaction, newFrontTaskId: Int, ): RunningTaskInfo? { - ProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d", - newFrontTaskId) - val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront( - taskRepository.getActiveNonMinimizedOrderedTasks(displayId), - newFrontTaskId) - val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack) + logV("addAndGetMinimizeTaskChanges, newFrontTask=%d", newFrontTaskId) + // This list is ordered from front to back. + val newTaskOrderedList = createOrderedTaskListWithNewTask( + taskRepository.getExpandedTasksOrdered(displayId), newFrontTaskId) + val taskToMinimize = getTaskToMinimize(newTaskOrderedList) if (taskToMinimize != null) { wct.reorder(taskToMinimize.token, false /* onTop */) return taskToMinimize @@ -229,7 +204,7 @@ class DesktopTasksLimiter ( } /** - * Add a pending minimize transition change, to update the list of minimized apps once the + * Add a pending minimize transition change to update the list of minimized apps once the * transition goes through. */ fun addPendingMinimizeChange(transition: IBinder, displayId: Int, taskId: Int) { @@ -238,51 +213,47 @@ class DesktopTasksLimiter ( } /** - * Returns the Task to minimize given 1. a list of visible tasks ordered from front to back and - * 2. a new task placed in front of all the others. + * Returns the minimized task from the list of visible tasks ordered from front to back with + * the new task placed in front of other tasks. */ - fun getTaskToMinimizeIfNeeded( - visibleFreeformTaskIdsOrderedFrontToBack: List<Int>, + fun getTaskToMinimize( + visibleOrderedTasks: List<Int>, newTaskIdInFront: Int - ): RunningTaskInfo? { - return getTaskToMinimizeIfNeeded( - createOrderedTaskListWithGivenTaskInFront( - visibleFreeformTaskIdsOrderedFrontToBack, newTaskIdInFront)) - } + ): RunningTaskInfo? = + getTaskToMinimize(createOrderedTaskListWithNewTask(visibleOrderedTasks, newTaskIdInFront)) /** Returns the Task to minimize given a list of visible tasks ordered from front to back. */ - fun getTaskToMinimizeIfNeeded( - visibleFreeformTaskIdsOrderedFrontToBack: List<Int> - ): RunningTaskInfo? { - if (visibleFreeformTaskIdsOrderedFrontToBack.size <= maxTasksLimit) { - ProtoLog.v( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: no need to minimize; tasks below limit") - // No need to minimize anything + fun getTaskToMinimize(visibleOrderedTasks: List<Int>): RunningTaskInfo? { + if (visibleOrderedTasks.size <= maxTasksLimit) { + logV("No need to minimize; tasks below limit") return null } - val taskIdToMinimize = visibleFreeformTaskIdsOrderedFrontToBack.last() + val taskIdToMinimize = visibleOrderedTasks.last() val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) if (taskToMinimize == null) { - ProtoLog.e( - ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, - "DesktopTasksLimiter: taskToMinimize(taskId = %d) == null", - taskIdToMinimize, - ) + logE("taskToMinimize(taskId = %d) == null", taskIdToMinimize) return null } return taskToMinimize } - private fun createOrderedTaskListWithGivenTaskInFront( - existingTaskIdsOrderedFrontToBack: List<Int>, - newTaskId: Int - ): List<Int> { - return listOf(newTaskId) + - existingTaskIdsOrderedFrontToBack.filter { taskId -> taskId != newTaskId } - } + private fun createOrderedTaskListWithNewTask( + orderedTaskIds: List<Int>, newTaskId: Int): List<Int> = + listOf(newTaskId) + orderedTaskIds.filter { taskId -> taskId != newTaskId } @VisibleForTesting fun getTransitionObserver(): TransitionObserver = minimizeTransitionObserver -}
\ No newline at end of file + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private fun logE(msg: String, vararg arguments: Any?) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private companion object { + const val TAG = "DesktopTasksLimiter" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index 771573d48e45..7631ece761b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -28,7 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.window.flags.Flags; -import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler; +import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; @@ -48,7 +48,7 @@ import java.util.Optional; */ public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver { private final Transitions mTransitions; - private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler; + private final Optional<DesktopImmersiveController> mDesktopImmersiveController; private final WindowDecorViewModel mWindowDecorViewModel; private final Optional<TaskChangeListener> mTaskChangeListener; private final FocusTransitionObserver mFocusTransitionObserver; @@ -60,12 +60,12 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs Context context, ShellInit shellInit, Transitions transitions, - Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler, + Optional<DesktopImmersiveController> desktopImmersiveController, WindowDecorViewModel windowDecorViewModel, Optional<TaskChangeListener> taskChangeListener, FocusTransitionObserver focusTransitionObserver) { mTransitions = transitions; - mImmersiveTransitionHandler = immersiveTransitionHandler; + mDesktopImmersiveController = desktopImmersiveController; mWindowDecorViewModel = windowDecorViewModel; mTaskChangeListener = taskChangeListener; mFocusTransitionObserver = focusTransitionObserver; @@ -89,7 +89,8 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository // is updated from there **before** the |mWindowDecorViewModel| methods are invoked. // Otherwise window decoration relayout won't run with the immersive state up to date. - mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition, info)); + mDesktopImmersiveController.ifPresent(h -> + h.onTransitionReady(transition, info, startT, finishT)); } // Update focus state first to ensure the correct state can be queried from listeners. // TODO(371503964): Remove this once the unified task repository is ready. @@ -194,10 +195,20 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs } @Override - public void onTransitionStarting(@NonNull IBinder transition) {} + public void onTransitionStarting(@NonNull IBinder transition) { + if (Flags.enableFullyImmersiveInDesktop()) { + // TODO(b/367268953): Remove when DesktopTaskListener is introduced. + mDesktopImmersiveController.ifPresent(h -> h.onTransitionStarting(transition)); + } + } @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { + if (Flags.enableFullyImmersiveInDesktop()) { + // TODO(b/367268953): Remove when DesktopTaskListener is introduced. + mDesktopImmersiveController.ifPresent(h -> h.onTransitionMerged(merged, playing)); + } + final List<ActivityManager.RunningTaskInfo> infoOfMerged = mTransitionToTaskInfo.get(merged); if (infoOfMerged == null) { @@ -218,6 +229,11 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { + if (Flags.enableFullyImmersiveInDesktop()) { + // TODO(b/367268953): Remove when DesktopTaskListener is introduced. + mDesktopImmersiveController.ifPresent(h -> h.onTransitionFinished(transition, aborted)); + } + final List<ActivityManager.RunningTaskInfo> taskInfo = mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList()); mTransitionToTaskInfo.remove(transition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 766a6b3f48ac..0d89f757903e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -83,8 +83,11 @@ public class DefaultMixedHandler implements MixedTransitionHandler, /** Both the display and split-state (enter/exit) is changing */ static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2; - /** Pip was entered while handling an intent with its own remoteTransition. */ - static final int TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE = 3; + /** + * While handling an intent with its own remoteTransition, a PIP enter or Desktop immersive + * exit change is found. + */ + static final int TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE = 3; /** Recents transition while split-screen foreground. */ static final int TYPE_RECENTS_DURING_SPLIT = 4; @@ -110,6 +113,9 @@ public class DefaultMixedHandler implements MixedTransitionHandler, /** The display changes when pip is entering. */ static final int TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE = 11; + /** Open transition during a desktop session. */ + static final int TYPE_OPEN_IN_DESKTOP = 12; + /** The default animation for this mixed transition. */ static final int ANIM_TYPE_DEFAULT = 0; @@ -296,7 +302,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, return null; } final MixedTransition mixed = createDefaultMixedTransition( - MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition); + MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE, transition); mixed.mLeftoversHandler = handler.first; mActiveTransitions.add(mixed); if (mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) { @@ -334,6 +340,20 @@ public class DefaultMixedHandler implements MixedTransitionHandler, MixedTransition.TYPE_UNFOLD, transition)); } return wct; + } else if (mDesktopTasksController != null + && mDesktopTasksController.shouldPlayDesktopAnimation(request)) { + final Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler = + mPlayer.dispatchRequest(transition, request, /* skip= */ this); + if (handler == null) { + return null; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a desktop request, so" + + " treat it as Mixed. handler=%s", handler.first); + final MixedTransition mixed = createDefaultMixedTransition( + MixedTransition.TYPE_OPEN_IN_DESKTOP, transition); + mixed.mLeftoversHandler = handler.first; + mActiveTransitions.add(mixed); + return handler.second; } return null; } @@ -341,7 +361,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, private DefaultMixedTransition createDefaultMixedTransition(int type, IBinder transition) { return new DefaultMixedTransition( type, transition, mPlayer, this, mPipHandler, mSplitHandler, mKeyguardHandler, - mUnfoldHandler, mActivityEmbeddingController); + mUnfoldHandler, mActivityEmbeddingController, mDesktopTasksController); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java index c8921d256d7f..3d3de88cdafc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -30,6 +30,7 @@ import android.window.TransitionInfo; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; +import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -39,15 +40,19 @@ import com.android.wm.shell.unfold.UnfoldTransitionHandler; class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { private final UnfoldTransitionHandler mUnfoldHandler; private final ActivityEmbeddingController mActivityEmbeddingController; + @Nullable + private final DesktopTasksController mDesktopTasksController; DefaultMixedTransition(int type, IBinder transition, Transitions player, MixedTransitionHandler mixedHandler, PipTransitionController pipHandler, StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler, UnfoldTransitionHandler unfoldHandler, - ActivityEmbeddingController activityEmbeddingController) { + ActivityEmbeddingController activityEmbeddingController, + @Nullable DesktopTasksController desktopTasksController) { super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler); mUnfoldHandler = unfoldHandler; mActivityEmbeddingController = activityEmbeddingController; + mDesktopTasksController = desktopTasksController; switch (type) { case TYPE_UNFOLD: @@ -57,7 +62,8 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: case TYPE_ENTER_PIP_FROM_SPLIT: case TYPE_KEYGUARD: - case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: + case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE: + case TYPE_OPEN_IN_DESKTOP: default: break; } @@ -85,11 +91,14 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { case TYPE_KEYGUARD -> animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback, mKeyguardHandler, mPipHandler); - case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE -> - animateOpenIntentWithRemoteAndPip(transition, info, startTransaction, + case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE -> + animateOpenIntentWithRemoteAndPipOrDesktop(transition, info, startTransaction, finishTransaction, finishCallback); case TYPE_UNFOLD -> animateUnfold(info, startTransaction, finishTransaction, finishCallback); + case TYPE_OPEN_IN_DESKTOP -> + animateOpenInDesktop( + transition, info, startTransaction, finishTransaction, finishCallback); default -> throw new IllegalStateException( "Starting default mixed animation with unknown or illegal type: " + mType); }; @@ -146,31 +155,34 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { return true; } - private boolean animateOpenIntentWithRemoteAndPip( + private boolean animateOpenIntentWithRemoteAndPipOrDesktop( @NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for opening an intent" - + " with a remote transition and PIP #%d", info.getDebugId()); - boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip( + + " with a remote transition and PIP or Desktop #%d", info.getDebugId()); + boolean handledToPipOrDesktop = tryAnimateOpenIntentWithRemoteAndPipOrDesktop( info, startTransaction, finishTransaction, finishCallback); // Consume the transition on remote handler if the leftover handler already handle this // transition. And if it cannot, the transition will be handled by remote handler, so don't // consume here. - // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip - if (handledToPip && mHasRequestToRemote + // Need to check leftOverHandler as it may change in + // #animateOpenIntentWithRemoteAndPipOrDesktop + if (handledToPipOrDesktop && mHasRequestToRemote && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) { mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null); } - return handledToPip; + return handledToPipOrDesktop; } - private boolean tryAnimateOpenIntentWithRemoteAndPip( + private boolean tryAnimateOpenIntentWithRemoteAndPipOrDesktop( @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "tryAnimateOpenIntentWithRemoteAndPipOrDesktop"); TransitionInfo.Change pipChange = null; for (int i = info.getChanges().size() - 1; i >= 0; --i) { TransitionInfo.Change change = info.getChanges().get(i); @@ -183,13 +195,31 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { info.getChanges().remove(i); } } + TransitionInfo.Change desktopChange = null; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (mDesktopTasksController != null + && mDesktopTasksController.isDesktopChange(mTransition, change)) { + if (desktopChange != null) { + throw new IllegalStateException("More than 1 desktop changes in one" + + " transition? " + info); + } + desktopChange = change; + info.getChanges().remove(i); + } + } Transitions.TransitionFinishCallback finishCB = (wct) -> { --mInFlightSubAnimations; joinFinishArgs(wct); if (mInFlightSubAnimations > 0) return; finishCallback.onTransitionFinished(mFinishWCT); }; - if (pipChange == null) { + if ((pipChange == null && desktopChange == null) + || (pipChange != null && desktopChange != null)) { + // Don't split the transition. Let the leftovers handler handle it all. + // TODO: b/? - split the transition into three pieces when there's both a PIP and a + // desktop change are present. For example, during remote intent open over a desktop + // with both a PIP capable task and an immersive task. if (mLeftoversHandler != null) { mInFlightSubAnimations = 1; if (mLeftoversHandler.startAnimation( @@ -198,27 +228,52 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { } } return false; - } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate" - + " animation because remote-animation likely doesn't support it #%d", - info.getDebugId()); - // Split the transition into 2 parts: the pip part and the rest. - mInFlightSubAnimations = 2; - // make a new startTransaction because pip's startEnterAnimation "consumes" it so - // we need a separate one to send over to launcher. - SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); + } else if (pipChange != null && desktopChange == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate" + + " animation because remote-animation likely doesn't support it #%d", + info.getDebugId()); + // Split the transition into 2 parts: the pip part and the rest. + mInFlightSubAnimations = 2; + // make a new startTransaction because pip's startEnterAnimation "consumes" it so + // we need a separate one to send over to launcher. + SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); + + mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB); + + // Dispatch the rest of the transition normally. + if (mLeftoversHandler != null + && mLeftoversHandler.startAnimation(mTransition, info, + startTransaction, finishTransaction, finishCB)) { + return true; + } + mLeftoversHandler = mPlayer.dispatchTransition( + mTransition, info, startTransaction, finishTransaction, finishCB, + mMixedHandler); + return true; + } else if (pipChange == null && desktopChange != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting desktop change into a" + + "separate animation because remote-animation likely doesn't support" + + "it #%d", info.getDebugId()); + mInFlightSubAnimations = 2; + SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); - mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB); + mDesktopTasksController.animateDesktopChange( + mTransition, desktopChange, otherStartT, finishTransaction, finishCB); - // Dispatch the rest of the transition normally. - if (mLeftoversHandler != null - && mLeftoversHandler.startAnimation(mTransition, info, - startTransaction, finishTransaction, finishCB)) { + // Dispatch the rest of the transition normally. + if (mLeftoversHandler != null + && mLeftoversHandler.startAnimation(mTransition, info, + startTransaction, finishTransaction, finishCB)) { + return true; + } + mLeftoversHandler = mPlayer.dispatchTransition( + mTransition, info, startTransaction, finishTransaction, finishCB, + mMixedHandler); return true; + } else { + throw new IllegalStateException( + "All PIP and Immersive combinations should've been handled"); } - mLeftoversHandler = mPlayer.dispatchTransition( - mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler); - return true; } private boolean animateUnfold( @@ -246,6 +301,51 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { mTransition, info, startTransaction, finishTransaction, finishCB); } + private boolean animateOpenInDesktop( + @NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "animateOpenInDesktop"); + TransitionInfo.Change desktopChange = null; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (mDesktopTasksController.isDesktopChange(mTransition, change)) { + if (desktopChange != null) { + throw new IllegalStateException("More than 1 desktop changes in one" + + " transition? " + info); + } + desktopChange = change; + info.getChanges().remove(i); + } + } + final Transitions.TransitionFinishCallback finishCB = (wct) -> { + --mInFlightSubAnimations; + joinFinishArgs(wct); + if (mInFlightSubAnimations > 0) return; + finishCallback.onTransitionFinished(mFinishWCT); + }; + if (desktopChange == null) { + if (mLeftoversHandler != null) { + mInFlightSubAnimations = 1; + if (mLeftoversHandler.startAnimation( + mTransition, info, startTransaction, finishTransaction, finishCB)) { + return true; + } + } + return false; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting desktop change into a" + + "separate animation #%d", info.getDebugId()); + mInFlightSubAnimations = 2; + mDesktopTasksController.animateDesktopChange( + transition, desktopChange, startTransaction, finishTransaction, finishCB); + mLeftoversHandler = mPlayer.dispatchTransition( + mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler); + return true; + } + @Override void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, @@ -279,7 +379,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { case TYPE_KEYGUARD: mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); return; - case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: + case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE: mPipHandler.end(); if (mLeftoversHandler != null) { mLeftoversHandler.mergeAnimation( @@ -289,6 +389,10 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { case TYPE_UNFOLD: mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); return; + case TYPE_OPEN_IN_DESKTOP: + mDesktopTasksController.mergeAnimation( + transition, info, t, mergeTarget, finishCallback); + return; default: throw new IllegalStateException("Playing a default mixed transition with unknown or" + " illegal type: " + mType); @@ -310,12 +414,14 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { case TYPE_KEYGUARD: mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT); break; - case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: + case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE: mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); break; case TYPE_UNFOLD: mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT); break; + case TYPE_OPEN_IN_DESKTOP: + mDesktopTasksController.onTransitionConsumed(transition, aborted, finishT); default: break; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 346f21b86e65..7c9cd0862b69 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -18,6 +18,7 @@ package com.android.wm.shell.transition; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; @@ -58,6 +59,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemProperties; +import android.os.Trace; import android.provider.Settings; import android.util.ArrayMap; import android.util.Log; @@ -800,8 +802,17 @@ public class Transitions implements RemoteCallable<Transitions>, track.mReadyTransitions.add(active); for (int i = 0; i < mObservers.size(); ++i) { + final boolean useTrace = Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER); + if (useTrace) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, + mObservers.get(i).getClass().getSimpleName() + "#onTransitionReady: " + + transitTypeToString(info.getType())); + } mObservers.get(i).onTransitionReady( active.mToken, info, active.mStartT, active.mFinishT); + if (useTrace) { + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + } } /* @@ -931,7 +942,7 @@ public class Transitions implements RemoteCallable<Transitions>, onFinish(ready.mToken, null); return; } - playTransition(ready); + playTransitionWithTracing(ready); // Attempt to merge any more queued-up transitions. processReadyQueue(track); return; @@ -1003,6 +1014,18 @@ public class Transitions implements RemoteCallable<Transitions>, processReadyQueue(track); } + private void playTransitionWithTracing(@NonNull ActiveTransition active) { + final boolean useTrace = Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER); + if (useTrace) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, + "playTransition: " + transitTypeToString(active.mInfo.getType())); + } + playTransition(active); + if (useTrace) { + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + } + } + private void playTransition(@NonNull ActiveTransition active) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active); final var token = active.mToken; @@ -1022,6 +1045,12 @@ public class Transitions implements RemoteCallable<Transitions>, if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); mTransitionTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler); + if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) { + Trace.instant(TRACE_TAG_WINDOW_MANAGER, + active.mHandler.getClass().getSimpleName() + + "#startAnimation animated " + + transitTypeToString(active.mInfo.getType())); + } return; } } @@ -1052,6 +1081,12 @@ public class Transitions implements RemoteCallable<Transitions>, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s", mHandlers.get(i)); mTransitionTracer.logDispatched(info.getDebugId(), mHandlers.get(i)); + if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) { + Trace.instant(TRACE_TAG_WINDOW_MANAGER, + mHandlers.get(i).getClass().getSimpleName() + + "#startAnimation animated " + + transitTypeToString(info.getType())); + } return mHandlers.get(i); } } @@ -1059,6 +1094,26 @@ public class Transitions implements RemoteCallable<Transitions>, "This shouldn't happen, maybe the default handler is broken."); } + private Pair<TransitionHandler, WindowContainerTransaction> dispatchRequestWithTracing( + @NonNull IBinder transition, @NonNull TransitionRequestInfo request, + @Nullable TransitionHandler skip) { + final boolean useTrace = Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER); + if (useTrace) { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, + "dispatchRequest: " + transitTypeToString(request.getType())); + } + Pair<TransitionHandler, WindowContainerTransaction> result = + dispatchRequest(transition, request, skip); + if (useTrace) { + if (result != null) { + Trace.instant(TRACE_TAG_WINDOW_MANAGER, result.first.getClass().getSimpleName() + + "#handleRequest handled " + transitTypeToString(request.getType())); + } + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + } + return result; + } + /** * Gives every handler (in order) a chance to handle request until one consumes the transition. * @return the WindowContainerTransaction given by the handler which consumed the transition. @@ -1197,12 +1252,11 @@ public class Transitions implements RemoteCallable<Transitions>, mSleepHandler.handleRequest(transitionToken, request); active.mHandler = mSleepHandler; } else { - for (int i = mHandlers.size() - 1; i >= 0; --i) { - wct = mHandlers.get(i).handleRequest(transitionToken, request); - if (wct != null) { - active.mHandler = mHandlers.get(i); - break; - } + Pair<TransitionHandler, WindowContainerTransaction> requestResult = + dispatchRequestWithTracing(transitionToken, request, /* skip= */ null); + if (requestResult != null) { + active.mHandler = requestResult.first; + wct = requestResult.second; } if (request.getDisplayChange() != null) { TransitionRequestInfo.DisplayChange change = request.getDisplayChange(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java index 60c922293d80..78e7962dcec3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java @@ -29,8 +29,6 @@ import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.window.DesktopModeFlags; -import androidx.annotation.NonNull; - import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; @@ -129,13 +127,15 @@ public class DragPositioningCallbackUtility { // If width or height are negative or exceeding the width or height constraints, revert the // respective bounds to use previous bound dimensions. - if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController, + if (isExceedingWidthConstraint(repositionTaskBounds.width(), + /* startingWidth= */ oldRight - oldLeft, stableBounds, displayController, windowDecoration)) { repositionTaskBounds.right = oldRight; repositionTaskBounds.left = oldLeft; isAspectRatioMaintained = false; } - if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController, + if (isExceedingHeightConstraint(repositionTaskBounds.height(), + /* startingHeight= */oldBottom - oldTop, stableBounds, displayController, windowDecoration)) { repositionTaskBounds.top = oldTop; repositionTaskBounds.bottom = oldBottom; @@ -208,28 +208,34 @@ public class DragPositioningCallbackUtility { return result; } - private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds, + private static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth, Rect maxResizeBounds, DisplayController displayController, WindowDecoration windowDecoration) { + boolean isSizeIncreasing = (repositionedWidth - startingWidth) > 0; // Check if width is less than the minimum width constraint. - if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) { - return true; + if (repositionedWidth < getMinWidth(displayController, windowDecoration)) { + // Only allow width to be increased if it is already below minimum. + return !isSizeIncreasing; } // Check if width is more than the maximum resize bounds on desktop windowing mode. + // Only allow width to be decreased if it already exceeds maximum. return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) - && repositionTaskBounds.width() > maxResizeBounds.width(); + && repositionedWidth > maxResizeBounds.width() && isSizeIncreasing; } - private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds, + private static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight, Rect maxResizeBounds, DisplayController displayController, WindowDecoration windowDecoration) { + boolean isSizeIncreasing = (repositionedHeight - startingHeight) > 0; // Check if height is less than the minimum height constraint. - if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) { - return true; + if (repositionedHeight < getMinHeight(displayController, windowDecoration)) { + // Only allow height to be increased if it is already below minimum. + return !isSizeIncreasing; } // Check if height is more than the maximum resize bounds on desktop windowing mode. + // Only allow height to be decreased if it already exceeds maximum. return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext) - && repositionTaskBounds.height() > maxResizeBounds.height(); + && repositionedHeight > maxResizeBounds.height() && isSizeIncreasing; } private static float getMinWidth(DisplayController displayController, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java index d6059a88e9c7..8fd7c0ec3099 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java @@ -16,6 +16,10 @@ package com.android.wm.shell.compatui; +import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_HIDDEN; + +import static junit.framework.Assert.assertEquals; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -25,7 +29,6 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -63,13 +66,75 @@ public class CompatUIStatusManagerTest extends ShellTestCase { assertFalse(mStatusManager.isEducationVisible()); } + @Test + public void valuesAreCached() { + // At the beginning the value is not read or written because + // we access the reader in lazy way. + mTestState.assertReaderInvocations(0); + mTestState.assertWriterInvocations(0); + + // We read the value when we start. Initial value is hidden. + assertFalse(mStatusManager.isEducationVisible()); + mTestState.assertReaderInvocations(1); + mTestState.assertWriterInvocations(0); + + // We send the event for the same state which is not written. + mStatusManager.onEducationHidden(); + assertFalse(mStatusManager.isEducationVisible()); + mTestState.assertReaderInvocations(1); + mTestState.assertWriterInvocations(0); + + // We send the event for the different state which is written but + // not read again. + mStatusManager.onEducationShown(); + assertTrue(mStatusManager.isEducationVisible()); + mTestState.assertReaderInvocations(1); + mTestState.assertWriterInvocations(1); + + // We read multiple times and we don't read the value again + mStatusManager.isEducationVisible(); + mStatusManager.isEducationVisible(); + mStatusManager.isEducationVisible(); + mTestState.assertReaderInvocations(1); + mTestState.assertWriterInvocations(1); + + // We write different values. Writer is only accessed when + // the value changes. + mStatusManager.onEducationHidden(); // change + mStatusManager.onEducationHidden(); + mStatusManager.onEducationShown(); // change + mStatusManager.onEducationShown(); + mStatusManager.onEducationHidden(); // change + mStatusManager.onEducationShown(); // change + mTestState.assertReaderInvocations(1); + mTestState.assertWriterInvocations(5); + } + static class FakeCompatUIStatusManagerTest { - int mCurrentStatus = 0; + int mCurrentStatus = COMPAT_UI_EDUCATION_HIDDEN; + + int mReaderInvocations; + + int mWriterInvocations; + + final IntSupplier mReader = () -> { + mReaderInvocations++; + return mCurrentStatus; + }; + + final IntConsumer mWriter = newStatus -> { + mWriterInvocations++; + mCurrentStatus = newStatus; + }; - final IntSupplier mReader = () -> mCurrentStatus; + void assertWriterInvocations(int expected) { + assertEquals(expected, mWriterInvocations); + } - final IntConsumer mWriter = newStatus -> mCurrentStatus = newStatus; + void assertReaderInvocations(int expected) { + assertEquals(expected, mReaderInvocations); + } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt index ef99b000d759..e83f5c7a79a1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt @@ -15,6 +15,7 @@ */ package com.android.wm.shell.desktopmode +import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS import android.graphics.Rect import android.os.Binder @@ -58,13 +59,13 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever /** - * Tests for [DesktopFullImmersiveTransitionHandler]. + * Tests for [DesktopImmersiveController]. * - * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandlerTest + * Usage: atest WMShellUnitTests:DesktopImmersiveControllerTest */ @SmallTest @RunWith(AndroidTestingRunner::class) -class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { +class DesktopImmersiveControllerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() @@ -75,7 +76,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var mockDisplayLayout: DisplayLayout private val transactionSupplier = { SurfaceControl.Transaction() } - private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler + private lateinit var controller: DesktopImmersiveController @Before fun setUp() { @@ -87,7 +88,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { invocation -> (invocation.getArgument(0) as Rect).set(STABLE_BOUNDS) } - immersiveHandler = DesktopFullImmersiveTransitionHandler( + controller = DesktopImmersiveController( transitions = mockTransitions, desktopRepository = desktopRepository, displayController = mockDisplayController, @@ -100,7 +101,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { fun enterImmersive_transitionReady_updatesRepository() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) - whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))) + whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) .thenReturn(mockBinder) desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, @@ -108,16 +109,14 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = false ) - immersiveHandler.moveTaskToImmersive(task) - immersiveHandler.onTransitionReady( + controller.moveTaskToImmersive(task) + controller.onTransitionReady( transition = mockBinder, info = createTransitionInfo( - changes = listOf( - TransitionInfo.Change(task.token, SurfaceControl()).apply { - taskInfo = task - } - ) - ) + changes = listOf(createChange(task)) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), ) assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue() @@ -128,7 +127,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { fun enterImmersive_savesPreImmersiveBounds() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) - whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))) + whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) .thenReturn(mockBinder) desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, @@ -137,16 +136,14 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { ) assertThat(desktopRepository.removeBoundsBeforeFullImmersive(task.taskId)).isNull() - immersiveHandler.moveTaskToImmersive(task) - immersiveHandler.onTransitionReady( + controller.moveTaskToImmersive(task) + controller.onTransitionReady( transition = mockBinder, info = createTransitionInfo( - changes = listOf( - TransitionInfo.Change(task.token, SurfaceControl()).apply { - taskInfo = task - } - ) - ) + changes = listOf(createChange(task)) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), ) assertThat(desktopRepository.removeBoundsBeforeFullImmersive(task.taskId)).isNotNull() @@ -156,7 +153,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { fun exitImmersive_transitionReady_updatesRepository() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) - whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))) + whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) .thenReturn(mockBinder) desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, @@ -164,16 +161,14 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) - immersiveHandler.moveTaskToNonImmersive(task) - immersiveHandler.onTransitionReady( + controller.moveTaskToNonImmersive(task) + controller.onTransitionReady( transition = mockBinder, info = createTransitionInfo( - changes = listOf( - TransitionInfo.Change(task.token, SurfaceControl()).apply { - taskInfo = task - } - ) - ) + changes = listOf(createChange(task)) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), ) assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse() @@ -184,7 +179,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { fun exitImmersive_onTransitionReady_removesBoundsBeforeImmersive() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) - whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))) + whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) .thenReturn(mockBinder) desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, @@ -193,16 +188,14 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { ) desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600)) - immersiveHandler.moveTaskToNonImmersive(task) - immersiveHandler.onTransitionReady( + controller.moveTaskToNonImmersive(task) + controller.onTransitionReady( transition = mockBinder, info = createTransitionInfo( - changes = listOf( - TransitionInfo.Change(task.token, SurfaceControl()).apply { - taskInfo = task - } - ) - ) + changes = listOf(createChange(task)) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), ) assertThat(desktopRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() @@ -217,16 +210,15 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) - immersiveHandler.onTransitionReady( + controller.onTransitionReady( transition = mock(IBinder::class.java), info = createTransitionInfo( - changes = listOf( - TransitionInfo.Change(task.token, SurfaceControl()).apply { - taskInfo = task - setRotation(/* start= */ Surface.ROTATION_0, /* end= */ Surface.ROTATION_90) - } - ) - ) + changes = listOf(createChange(task).apply { + setRotation(/* start= */ Surface.ROTATION_0, /* end= */ Surface.ROTATION_90) + }) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), ) assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse() @@ -236,28 +228,28 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { fun enterImmersive_inProgress_ignores() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) - whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))) + whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) .thenReturn(mockBinder) - immersiveHandler.moveTaskToImmersive(task) - immersiveHandler.moveTaskToImmersive(task) + controller.moveTaskToImmersive(task) + controller.moveTaskToImmersive(task) verify(mockTransitions, times(1)) - .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)) + .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)) } @Test fun exitImmersive_inProgress_ignores() { val task = createFreeformTask() val mockBinder = mock(IBinder::class.java) - whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))) + whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))) .thenReturn(mockBinder) - immersiveHandler.moveTaskToNonImmersive(task) - immersiveHandler.moveTaskToNonImmersive(task) + controller.moveTaskToNonImmersive(task) + controller.moveTaskToNonImmersive(task) verify(mockTransitions, times(1)) - .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)) + .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)) } @Test @@ -273,9 +265,9 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) - assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit -> + assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isTrue() @@ -294,9 +286,9 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = false ) - immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) - assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit -> + assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() @@ -315,7 +307,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) assertThat(wct.hasBoundsChange(task.token)).isTrue() } @@ -333,13 +325,38 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = false ) - immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) assertThat(wct.hasBoundsChange(task.token)).isFalse() } @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun exitImmersiveIfApplicable_byDisplay_withExcludeTask_doesNotExit() { + val task = createFreeformTask() + whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + val wct = WindowContainerTransaction() + val transition = Binder() + desktopRepository.setTaskInFullImmersiveState( + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + immersive = true + ) + + controller.exitImmersiveIfApplicable( + wct = wct, + displayId = DEFAULT_DISPLAY, + excludeTaskId = task.taskId + )?.invoke(transition) + + assertThat(controller.pendingExternalExitTransitions.any { exit -> + exit.transition == transition && exit.displayId == DEFAULT_DISPLAY + && exit.taskId == task.taskId + }).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun exitImmersiveIfApplicable_byTask_inImmersive_changesTaskBounds() { val task = createFreeformTask() whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) @@ -350,7 +367,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task) + controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task) assertThat(wct.hasBoundsChange(task.token)).isTrue() } @@ -367,7 +384,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = false ) - immersiveHandler.exitImmersiveIfApplicable(wct, task) + controller.exitImmersiveIfApplicable(wct, task) assertThat(wct.hasBoundsChange(task.token)).isFalse() } @@ -385,9 +402,9 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(transition) + controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition) - assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit -> + assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isTrue() @@ -406,9 +423,9 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = false ) - immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(transition) + controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition) - assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit -> + assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() @@ -416,7 +433,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun onTransitionReady_pendingExit_removesPendingExit() { + fun onTransitionReady_pendingExit_removesPendingExitOnFinish() { val task = createFreeformTask() whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) val wct = WindowContainerTransaction() @@ -426,18 +443,19 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { taskId = task.taskId, immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) - immersiveHandler.onTransitionReady( + controller.onTransitionReady( transition = transition, info = createTransitionInfo( - changes = listOf( - TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } - ) - ) + changes = listOf(createChange(task)) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), ) + controller.onTransitionFinished(transition, aborted = false) - assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit -> + assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY && exit.taskId == task.taskId }).isFalse() @@ -445,6 +463,42 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun onTransitionReady_pendingExit_withMerge_removesPendingExitOnFinish() { + val task = createFreeformTask() + whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + val wct = WindowContainerTransaction() + val transition = Binder() + val mergedToTransition = Binder() + desktopRepository.setTaskInFullImmersiveState( + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + immersive = true + ) + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + + controller.onTransitionReady( + transition = transition, + info = createTransitionInfo( + changes = listOf(createChange(task)) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), + ) + controller.onTransitionMerged(transition, mergedToTransition) + controller.onTransitionFinished(mergedToTransition, aborted = false) + + assertThat(controller.pendingExternalExitTransitions.any { exit -> + exit.transition == transition && exit.displayId == DEFAULT_DISPLAY + && exit.taskId == task.taskId + }).isFalse() + assertThat(controller.pendingExternalExitTransitions.any { exit -> + exit.transition == mergedToTransition && exit.displayId == DEFAULT_DISPLAY + && exit.taskId == task.taskId + }).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun onTransitionReady_pendingExit_updatesRepository() { val task = createFreeformTask() whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) @@ -455,15 +509,15 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { taskId = task.taskId, immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) - immersiveHandler.onTransitionReady( + controller.onTransitionReady( transition = transition, info = createTransitionInfo( - changes = listOf( - TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } - ) - ) + changes = listOf(createChange(task)) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), ) assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse() @@ -485,15 +539,15 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600)) - immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) - immersiveHandler.onTransitionReady( + controller.onTransitionReady( transition = transition, info = createTransitionInfo( - changes = listOf( - TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } - ) - ) + changes = listOf(createChange(task)) + ), + startTransaction = SurfaceControl.Transaction(), + finishTransaction = SurfaceControl.Transaction(), ) assertThat(desktopRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() @@ -512,7 +566,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task) + controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task) assertThat( wct.hasBoundsChange(task.token, calculateMaximizeBounds(mockDisplayLayout, task)) @@ -536,7 +590,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { val preImmersiveBounds = Rect(100, 100, 500, 500) desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, preImmersiveBounds) - immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task) + controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task) assertThat( wct.hasBoundsChange(task.token, preImmersiveBounds) @@ -559,7 +613,7 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task) + controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task) assertThat( wct.hasBoundsChange(task.token, calculateInitialBounds(mockDisplayLayout, task)) @@ -577,13 +631,32 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { taskId = task.taskId, immersive = true ) - immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(Binder()) + controller.exitImmersiveIfApplicable(wct, task)?.invoke(Binder()) - immersiveHandler.moveTaskToNonImmersive(task) + controller.moveTaskToNonImmersive(task) verify(mockTransitions, never()).startTransition(any(), any(), any()) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun exitImmersiveIfApplicable_inImmersive_isImmersiveChange() { + val task = createFreeformTask() + whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + val wct = WindowContainerTransaction() + val transition = Binder() + val change = createChange(task) + desktopRepository.setTaskInFullImmersiveState( + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + immersive = true + ) + + controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY) + + assertThat(controller.isImmersiveChange(transition, change)).isTrue() + } + private fun createTransitionInfo( @TransitionType type: Int = TRANSIT_CHANGE, @TransitionFlags flags: Int = 0, @@ -592,6 +665,11 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() { changes.forEach { change -> addChange(change) } } + private fun createChange(task: RunningTaskInfo): TransitionInfo.Change = + TransitionInfo.Change(task.token, SurfaceControl()).apply { + taskInfo = task + } + private fun WindowContainerTransaction.hasBoundsChange(token: WindowContainerToken): Boolean = this.changes.any { change -> change.key == token.asBinder() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index 81d59d586dd3..50368377691f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -147,7 +147,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() { val transition = mock<IBinder>() val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)) - whenever(desktopRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(2) + whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) whenever( closeDesktopTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any()) ) @@ -170,7 +170,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() { val transition = mock<IBinder>() val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)) - whenever(desktopRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(1) + whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(1) whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any())) .thenReturn(mock()) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 3e2280393c2b..d90443c99d37 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -898,7 +898,7 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun getActiveNonMinimizedOrderedTasks_returnsFreeformTasksInCorrectOrder() { + fun getExpandedTasksOrdered_returnsFreeformTasksInCorrectOrder() { repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) @@ -907,13 +907,13 @@ class DesktopRepositoryTest : ShellTestCase() { repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 2) repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 1) - val tasks = repo.getActiveNonMinimizedOrderedTasks(displayId = 0) + val tasks = repo.getExpandedTasksOrdered(displayId = 0) assertThat(tasks).containsExactly(1, 2, 3).inOrder() } @Test - fun getActiveNonMinimizedOrderedTasks_excludesMinimizedTasks() { + fun getExpandedTasksOrdered_excludesMinimizedTasks() { repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) @@ -923,7 +923,7 @@ class DesktopRepositoryTest : ShellTestCase() { repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 1) repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) - val tasks = repo.getActiveNonMinimizedOrderedTasks(displayId = DEFAULT_DISPLAY) + val tasks = repo.getExpandedTasksOrdered(displayId = DEFAULT_DISPLAY) assertThat(tasks).containsExactly(1, 3).inOrder() } 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 7c336cdb54f6..bc2b36ccd835 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 @@ -201,7 +201,7 @@ class DesktopTasksControllerTest : ShellTestCase() { lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler @Mock - lateinit var mockDesktopFullImmersiveTransitionHandler: DesktopFullImmersiveTransitionHandler + lateinit var mMockDesktopImmersiveController: DesktopImmersiveController @Mock lateinit var launchAdjacentController: LaunchAdjacentController @Mock lateinit var splitScreenController: SplitScreenController @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler @@ -322,7 +322,7 @@ class DesktopTasksControllerTest : ShellTestCase() { dragAndDropTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, - mockDesktopFullImmersiveTransitionHandler, + mMockDesktopImmersiveController, taskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, @@ -1773,7 +1773,7 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.minimizeTask(task) - verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(any(), eq(task)) + verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task)) } @Test @@ -1783,7 +1783,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val runOnTransit = RunOnStartTransitionCallback() whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) .thenReturn(transition) - whenever(mockDesktopFullImmersiveTransitionHandler.exitImmersiveIfApplicable(any(), eq(task))) + whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task))) .thenReturn(runOnTransit) controller.minimizeTask(task) @@ -3092,13 +3092,14 @@ class DesktopTasksControllerTest : ShellTestCase() { val transition = Binder() whenever(transitions.startTransition(eq(TRANSIT_OPEN), any(), anyOrNull())) .thenReturn(transition) - whenever(mockDesktopFullImmersiveTransitionHandler - .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId))).thenReturn(runOnStartTransit) + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId))) + .thenReturn(runOnStartTransit) runOpenInstance(immersiveTask, freeformTask.taskId) - verify(mockDesktopFullImmersiveTransitionHandler) - .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId)) + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId)) runOnStartTransit.assertOnlyInvocation(transition) } @@ -3445,7 +3446,7 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.toggleDesktopTaskFullImmersiveState(task) - verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToImmersive(task) + verify(mMockDesktopImmersiveController).moveTaskToImmersive(task) } @Test @@ -3455,7 +3456,7 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.toggleDesktopTaskFullImmersiveState(task) - verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task) + verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(task) } @Test @@ -3467,7 +3468,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task.requestedVisibleTypes = WindowInsets.Type.statusBars() controller.onTaskInfoChanged(task) - verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task) + verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(task) } @Test @@ -3479,7 +3480,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task.requestedVisibleTypes = WindowInsets.Type.statusBars() controller.onTaskInfoChanged(task) - verify(mockDesktopFullImmersiveTransitionHandler, never()).moveTaskToNonImmersive(task) + verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(task) } @Test @@ -3488,13 +3489,14 @@ class DesktopTasksControllerTest : ShellTestCase() { val wct = WindowContainerTransaction() val runOnStartTransit = RunOnStartTransitionCallback() val transition = Binder() - whenever(mockDesktopFullImmersiveTransitionHandler - .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit) + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit) whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) - verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId) + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(wct, task.displayId, task.taskId) runOnStartTransit.assertOnlyInvocation(transition) } @@ -3504,13 +3506,14 @@ class DesktopTasksControllerTest : ShellTestCase() { val wct = WindowContainerTransaction() val runOnStartTransit = RunOnStartTransitionCallback() val transition = Binder() - whenever(mockDesktopFullImmersiveTransitionHandler - .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit) + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit) whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) - verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId) + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(wct, task.displayId, task.taskId) runOnStartTransit.assertOnlyInvocation(transition) } @@ -3519,14 +3522,15 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = setUpFreeformTask(background = true) val runOnStartTransit = RunOnStartTransitionCallback() val transition = Binder() - whenever(mockDesktopFullImmersiveTransitionHandler - .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit) + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))) + .thenReturn(runOnStartTransit) whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) controller.moveTaskToFront(task.taskId, remoteTransition = null) - verify(mockDesktopFullImmersiveTransitionHandler) - .exitImmersiveIfApplicable(any(), eq(task.displayId)) + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)) runOnStartTransit.assertOnlyInvocation(transition) } @@ -3535,14 +3539,15 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = setUpFreeformTask(background = false) val runOnStartTransit = RunOnStartTransitionCallback() val transition = Binder() - whenever(mockDesktopFullImmersiveTransitionHandler - .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit) + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))) + .thenReturn(runOnStartTransit) whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) controller.moveTaskToFront(task.taskId, remoteTransition = null) - verify(mockDesktopFullImmersiveTransitionHandler) - .exitImmersiveIfApplicable(any(), eq(task.displayId)) + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)) runOnStartTransit.assertOnlyInvocation(transition) } @@ -3555,7 +3560,7 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.handleRequest(binder, createTransition(task)) - verify(mockDesktopFullImmersiveTransitionHandler) + verify(mMockDesktopImmersiveController) .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId)) } @@ -3567,10 +3572,117 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.handleRequest(binder, createTransition(task)) - verify(mockDesktopFullImmersiveTransitionHandler) + verify(mMockDesktopImmersiveController) .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId)) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() { + val triggerTask = setUpFullscreenTask(displayId = 5) + taskRepository.setTaskInFullImmersiveState( + displayId = triggerTask.displayId, + taskId = triggerTask.taskId, + immersive = true + ) + + assertThat(controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + )).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() { + val triggerTask = setUpFreeformTask(displayId = 5) + taskRepository.setTaskInFullImmersiveState( + displayId = triggerTask.displayId, + taskId = triggerTask.taskId, + immersive = true + ) + + assertThat(controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_CHANGE, triggerTask, /* remoteTransition= */ null) + )).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() { + val triggerTask = setUpFreeformTask(displayId = 5) + taskRepository.setTaskInFullImmersiveState( + displayId = triggerTask.displayId, + taskId = triggerTask.taskId, + immersive = false + ) + + assertThat(controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + )).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() { + // At least one freeform task to be in a desktop. + val existingTask = setUpFreeformTask(displayId = 5) + val triggerTask = setUpFullscreenTask(displayId = 5) + assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() + taskRepository.setTaskInFullImmersiveState( + displayId = existingTask.displayId, + taskId = existingTask.taskId, + immersive = true + ) + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + ) + ).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() { + val triggerTask = setUpFullscreenTask(displayId = 5) + assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() + + assertThat(controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + )).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() { + // At least one freeform task to be in a desktop. + val existingTask = setUpFreeformTask(displayId = 5) + val triggerTask = setUpFreeformTask(displayId = 5, active = false) + assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() + taskRepository.setTaskInFullImmersiveState( + displayId = existingTask.displayId, + taskId = existingTask.taskId, + immersive = true + ) + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + ) + ).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() { + val triggerTask = setUpFreeformTask(displayId = 5, active = false) + assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() + + assertThat(controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + )).isFalse() + } + private class RunOnStartTransitionCallback : ((IBinder) -> Unit) { var invocations = 0 private set diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 596b76dbdb2e..fa878d0bb077 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -303,12 +303,12 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChangesIfNeeded_tasksWithinLimit_noTaskMinimized() { + fun addAndGetMinimizeTaskChanges_tasksWithinLimit_noTaskMinimized() { (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val wct = WindowContainerTransaction() val minimizedTaskId = - desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + desktopTasksLimiter.addAndGetMinimizeTaskChanges( displayId = DEFAULT_DISPLAY, wct = wct, newFrontTaskId = setUpFreeformTask().taskId) @@ -318,13 +318,13 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChangesIfNeeded_tasksAboveLimit_backTaskMinimized() { + fun addAndGetMinimizeTaskChanges_tasksAboveLimit_backTaskMinimized() { // The following list will be ordered bottom -> top, as the last task is moved to top last. val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val wct = WindowContainerTransaction() val minimizedTaskId = - desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + desktopTasksLimiter.addAndGetMinimizeTaskChanges( displayId = DEFAULT_DISPLAY, wct = wct, newFrontTaskId = setUpFreeformTask().taskId) @@ -336,13 +336,13 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun addAndGetMinimizeTaskChangesIfNeeded_nonMinimizedTasksWithinLimit_noTaskMinimized() { + fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_noTaskMinimized() { val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) val wct = WindowContainerTransaction() val minimizedTaskId = - desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + desktopTasksLimiter.addAndGetMinimizeTaskChanges( displayId = 0, wct = wct, newFrontTaskId = setUpFreeformTask().taskId) @@ -352,46 +352,46 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test - fun getTaskToMinimizeIfNeeded_tasksWithinLimit_returnsNull() { + fun getTaskToMinimize_tasksWithinLimit_returnsNull() { val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( - visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + val minimizedTask = desktopTasksLimiter.getTaskToMinimize( + visibleOrderedTasks = tasks.map { it.taskId }) assertThat(minimizedTask).isNull() } @Test - fun getTaskToMinimizeIfNeeded_tasksAboveLimit_returnsBackTask() { + fun getTaskToMinimize_tasksAboveLimit_returnsBackTask() { val tasks = (1..MAX_TASK_LIMIT + 1).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( - visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + val minimizedTask = desktopTasksLimiter.getTaskToMinimize( + visibleOrderedTasks = tasks.map { it.taskId }) // first == front, last == back assertThat(minimizedTask).isEqualTo(tasks.last()) } @Test - fun getTaskToMinimizeIfNeeded_tasksAboveLimit_otherLimit_returnsBackTask() { + fun getTaskToMinimize_tasksAboveLimit_otherLimit_returnsBackTask() { desktopTasksLimiter = DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT2, interactionJankMonitor, mContext, handler) val tasks = (1..MAX_TASK_LIMIT2 + 1).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( - visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + val minimizedTask = desktopTasksLimiter.getTaskToMinimize( + visibleOrderedTasks = tasks.map { it.taskId }) // first == front, last == back assertThat(minimizedTask).isEqualTo(tasks.last()) } @Test - fun getTaskToMinimizeIfNeeded_withNewTask_tasksAboveLimit_returnsBackTask() { + fun getTaskToMinimize_withNewTask_tasksAboveLimit_returnsBackTask() { val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( - visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }, + val minimizedTask = desktopTasksLimiter.getTaskToMinimize( + visibleOrderedTasks = tasks.map { it.taskId }, newTaskIdInFront = setUpFreeformTask().taskId) // first == front, last == back diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java index 7ae0bcd13681..90ab2b8285cd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -43,7 +43,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; -import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler; +import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.TransitionInfoBuilder; @@ -70,7 +70,7 @@ public class FreeformTaskTransitionObserverTest { @Mock private Transitions mTransitions; @Mock - private DesktopFullImmersiveTransitionHandler mDesktopFullImmersiveTransitionHandler; + private DesktopImmersiveController mDesktopImmersiveController; @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock @@ -92,7 +92,7 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver = new FreeformTaskTransitionObserver( context, mShellInit, mTransitions, - Optional.of(mDesktopFullImmersiveTransitionHandler), + Optional.of(mDesktopImmersiveController), mWindowDecorViewModel, Optional.of(mTaskChangeListener), mFocusTransitionObserver); final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass( @@ -321,7 +321,7 @@ public class FreeformTaskTransitionObserverTest { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - public void onTransitionReady_forwardsToDesktopImmersiveHandler() { + public void onTransitionReady_forwardsToDesktopImmersiveController() { final IBinder transition = mock(IBinder.class); final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0).build(); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -329,7 +329,38 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionReady(transition, info, startT, finishT); - verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition, info); + verify(mDesktopImmersiveController).onTransitionReady(transition, info, startT, finishT); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + public void onTransitionMerged_forwardsToDesktopImmersiveController() { + final IBinder merged = mock(IBinder.class); + final IBinder playing = mock(IBinder.class); + + mTransitionObserver.onTransitionMerged(merged, playing); + + verify(mDesktopImmersiveController).onTransitionMerged(merged, playing); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + public void onTransitionStarting_forwardsToDesktopImmersiveController() { + final IBinder transition = mock(IBinder.class); + + mTransitionObserver.onTransitionStarting(transition); + + verify(mDesktopImmersiveController).onTransitionStarting(transition); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + public void onTransitionFinished_forwardsToDesktopImmersiveController() { + final IBinder transition = mock(IBinder.class); + + mTransitionObserver.onTransitionFinished(transition, /* aborted= */ false); + + verify(mDesktopImmersiveController).onTransitionFinished(transition, /* aborted= */ false); } private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt index 24f6becc3536..a20a89c644ed 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -36,6 +36,7 @@ import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.google.common.truth.Truth.assertThat @@ -48,9 +49,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.any -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import org.mockito.quality.Strictness +import org.mockito.Mockito.`when` as whenever /** * Tests for [DragPositioningCallbackUtility]. @@ -193,6 +194,62 @@ class DragPositioningCallbackUtilityTest { @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING) + fun testChangeBounds_unresizeableApp_initialHeightLessThanMin_increasingBounds_resizeAllowed() { + mockWindowDecoration.mTaskInfo.isResizeable = false + val startingPoint = PointF(BELOW_MIN_HEIGHT_BOUNDS.right.toFloat(), + BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(BELOW_MIN_HEIGHT_BOUNDS) + + // Resize to increased bounds + val newX = BELOW_MIN_HEIGHT_BOUNDS.right.toFloat() + 20 + val newY = BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat() + 10 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + // Resize should be allowed as drag is in direction of desired range + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, BELOW_MIN_HEIGHT_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.right + 20) + assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.bottom + 10) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING) + fun testChangeBounds_unresizeableApp_initialHeightMoreThanMax_decreasingBounds_resizeAllowed() { + mockWindowDecoration.mTaskInfo.isResizeable = false + val startingPoint = PointF(EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat(), + EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect(EXCEEDS_MAX_HEIGHT_BOUNDS) + + // Resize to decreased bounds. + val newX = EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat() - 10 + val newY = EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat() + 20 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + // Resize should be allowed as drag is in direction of desired range. + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + repositionTaskBounds, EXCEEDS_MAX_HEIGHT_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.top + 20) + assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.right - 10) + assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING) fun testChangeBounds_unresizeableApp_widthLessThanMin_resetToStartingBounds() { mockWindowDecoration.mTaskInfo.isResizeable = false val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat()) @@ -211,13 +268,68 @@ class DragPositioningCallbackUtilityTest { ) ) - assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left) assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top) assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right) assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING) + fun testChangeBounds_unresizeableApp_initialWidthLessThanMin_increasingBounds_resizeAllowed() { + mockWindowDecoration.mTaskInfo.isResizeable = false + val startingPoint = PointF(BELOW_MIN_WIDTH_BOUNDS.right.toFloat(), + BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(BELOW_MIN_WIDTH_BOUNDS) + + // Resize to increased bounds. + val newX = BELOW_MIN_WIDTH_BOUNDS.right.toFloat() + 10 + val newY = BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat() + 20 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + // Resize should be allowed as drag is in direction of desired range. + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, BELOW_MIN_WIDTH_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.right + 10) + assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.bottom + 20) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING) + fun testChangeBounds_unresizeableApp_initialWidthMoreThanMax_decreasingBounds_resizeAllowed() { + mockWindowDecoration.mTaskInfo.isResizeable = false + val startingPoint = PointF(EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat(), + EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect(EXCEEDS_MAX_WIDTH_BOUNDS) + + // Resize to decreased bounds. + val newX = EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat() + 20 + val newY = EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat() + 10 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + // Resize should be allowed as drag is in direction of desired range. + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_LEFT or CTRL_TYPE_TOP, + repositionTaskBounds, EXCEEDS_MAX_WIDTH_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.left + 20) + assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.top + 10) + assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.bottom) + } + @Test fun testChangeBoundsDoesNotChangeHeightWhenNegative() { @@ -427,6 +539,60 @@ class DragPositioningCallbackUtilityTest { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testMinHeight_initialHeightLessThanMin_increasingHeight_resizeAllowed() { + val startingPoint = PointF(BELOW_MIN_HEIGHT_BOUNDS.right.toFloat(), + BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(BELOW_MIN_HEIGHT_BOUNDS) + + // Attempt to increase height. + val newX = BELOW_MIN_HEIGHT_BOUNDS.right.toFloat() + val newY = BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat() + 10 + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + // Resize should be allowed as drag is increasing height closer to valid region. + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, BELOW_MIN_HEIGHT_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.bottom + 10) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testMinWidth_initialWidthLessThanMin_increasingBounds_resizeAllowed() { + val startingPoint = PointF(BELOW_MIN_WIDTH_BOUNDS.right.toFloat(), + BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat()) + val repositionTaskBounds = Rect(BELOW_MIN_WIDTH_BOUNDS) + + // Attempt to increase width. + val newX = BELOW_MIN_WIDTH_BOUNDS.right.toFloat() + 10 + val newY = BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat() + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + // Resize should be allowed as drag is increasing width closer to valid region. + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + repositionTaskBounds, BELOW_MIN_WIDTH_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.right + 10) + assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.bottom) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeAllowedSize_shouldChangeBounds() { doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(mockContext) } initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1) @@ -547,6 +713,61 @@ class DragPositioningCallbackUtilityTest { assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testMaxHeight_initialHeightMoreThanMax_decreasingHeight_resizeAllowed() { + mockWindowDecoration.mTaskInfo.isResizeable = false + val startingPoint = PointF(EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat(), + EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect(EXCEEDS_MAX_HEIGHT_BOUNDS) + + // Attempt to decrease height + val newX = EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat() - 10 + val newY = EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat() + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + // Resize should be allowed as drag is decreasing height closer to valid region. + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + repositionTaskBounds, EXCEEDS_MAX_HEIGHT_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.left) + assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.right - 10) + assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.bottom ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS) + fun testMaxHeight_initialWidthMoreThanMax_decreasingBounds_resizeAllowed() { + val startingPoint = PointF(EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat(), + EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat()) + val repositionTaskBounds = Rect(EXCEEDS_MAX_WIDTH_BOUNDS) + + // Attempt to decrease width. + val newX = EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat() + 20 + val newY = EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat() + val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint) + + // Resize should be allowed as drag is decreasing width closer to valid region. + assertTrue( + DragPositioningCallbackUtility.changeBounds( + CTRL_TYPE_LEFT or CTRL_TYPE_TOP, + repositionTaskBounds, EXCEEDS_MAX_WIDTH_BOUNDS, STABLE_BOUNDS, delta, + mockDisplayController, mockWindowDecoration + ) + ) + + assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.left + 20) + assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.top) + assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.right) + assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.bottom) + } + private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) { mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = TASK_ID @@ -571,6 +792,10 @@ class DragPositioningCallbackUtilityTest { private const val NAVBAR_HEIGHT = 50 private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val BELOW_MIN_WIDTH_BOUNDS = Rect(0, 0, 50, 100) + private val BELOW_MIN_HEIGHT_BOUNDS = Rect(0, 0, 100, 50) + private val EXCEEDS_MAX_WIDTH_BOUNDS = Rect(0, 0, 3000, 1500) + private val EXCEEDS_MAX_HEIGHT_BOUNDS = Rect(0, 0, 1000, 2000) private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10) private val DISALLOWED_RESIZE_AREA = Rect( DISPLAY_BOUNDS.left, diff --git a/libs/protoutil/Android.bp b/libs/protoutil/Android.bp index 28856c87f7c6..8af4b7e8f4c8 100644 --- a/libs/protoutil/Android.bp +++ b/libs/protoutil/Android.bp @@ -60,6 +60,7 @@ cc_library { "//apex_available:platform", "com.android.os.statsd", "test_com.android.os.statsd", + "com.android.uprobestats", ], } diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java index 13876ad6c3c7..e1fbfea19235 100644 --- a/media/java/android/media/AudioDeviceVolumeManager.java +++ b/media/java/android/media/AudioDeviceVolumeManager.java @@ -16,8 +16,11 @@ package android.media; +import static com.android.media.flags.Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL; + import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -434,6 +437,83 @@ public class AudioDeviceVolumeManager { /** * @hide + * Sets the input gain index for a particular AudioDeviceAttributes. + * TODO(b/364923030): create InputVolumeInfo on top of VolumeInfo rather than using index to + * handle volume information, to solve issues e.g. gain index ranges might be different for + * different categories of devices. + */ + @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void setInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) { + try { + getService().setInputGainIndex(ada, index); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Gets the input gain index for a particular AudioDeviceAttributes. + */ + @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int getInputGainIndex(@NonNull AudioDeviceAttributes ada) { + try { + return getService().getInputGainIndex(ada); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Gets the maximum input gain index for input device. + */ + @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int getMaxInputGainIndex() { + try { + return getService().getMaxInputGainIndex(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Gets the minimum input gain index for input device. + */ + @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int getMinInputGainIndex() { + try { + return getService().getMinInputGainIndex(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Indicates if an input device does not support input gain control. + * <p>The following APIs have no effect when input gain is fixed: + * <ul> + * <li>{@link #setInputGainIndex(AudioDeviceAttributes, int)} + * </ul> + */ + @FlaggedApi(FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) + @RequiresPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public boolean isInputGainFixed(@NonNull AudioDeviceAttributes ada) { + try { + return getService().isInputGainFixed(ada); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide * Return human-readable name for volume behavior * @param behavior one of the volume behaviors defined in AudioManager * @return a string for the given behavior diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index c22b67462af2..9fd3f5beb25c 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -189,6 +189,21 @@ interface IAudioService { void setMicrophoneMute(boolean on, String callingPackage, int userId, in String attributionTag); + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + void setInputGainIndex(in AudioDeviceAttributes ada, int index); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + int getInputGainIndex(in AudioDeviceAttributes ada); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + int getMaxInputGainIndex(); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + int getMinInputGainIndex(); + + @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") + boolean isInputGainFixed(in AudioDeviceAttributes ada); + oneway void setMicrophoneMuteFromSwitch(boolean on); void setRingerModeExternal(int ringerMode, String caller); diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 8ff4305a9817..3a19f466f7c1 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -462,6 +462,33 @@ public final class MediaCodecInfo { @SuppressLint("AllUpper") public static final int COLOR_FormatYUVP010 = 54; + /** + * P210 is 10-bit-per component 4:2:2 YCbCr semiplanar format. + * <p> + * This format uses 32 allocated bits per pixel with 20 bits of + * data per pixel. Chroma planes are subsampled by 2 both + * horizontally. Each chroma and luma component + * has 16 allocated bits in little-endian configuration with 10 + * MSB of actual data. + * + * <pre> + * byte byte + * <--------- i --------> | <------ i + 1 ------> + * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + * | UNUSED | Y/Cb/Cr | + * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + * 0 5 6 7 0 7 + * bit + * </pre> + * + * Use this format with {@link Image}. This format corresponds + * to {@link android.graphics.ImageFormat#YCBCR_P210}. + * <p> + */ + @SuppressLint("AllUpper") + @FlaggedApi(android.media.codec.Flags.FLAG_P210_FORMAT_SUPPORT) + public static final int COLOR_FormatYUVP210 = 60; + /** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */ public static final int COLOR_TI_FormatYUV420PackedSemiPlanar = 0x7f000100; // COLOR_FormatSurface indicates that the data will be a GraphicBuffer metadata reference. diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java index 41e9b65da93a..11bd221ec696 100644 --- a/media/java/android/media/Utils.java +++ b/media/java/android/media/Utils.java @@ -719,6 +719,9 @@ public class Utils { * @return {@code true} if the Uri has vibration parameter */ public static boolean hasVibration(Uri ringtoneUri) { + if (ringtoneUri == null) { + return false; + } final String vibrationUriString = ringtoneUri.getQueryParameter(VIBRATION_URI_PARAM); return vibrationUriString != null; } @@ -730,7 +733,10 @@ public class Utils { * @return parsed {@link Uri} of vibration parameter, {@code null} if the vibration parameter * is not found. */ - public static Uri getVibrationUri(Uri ringtoneUri) { + public static @Nullable Uri getVibrationUri(Uri ringtoneUri) { + if (ringtoneUri == null) { + return null; + } final String vibrationUriString = ringtoneUri.getQueryParameter(VIBRATION_URI_PARAM); if (vibrationUriString == null) { return null; diff --git a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java index fd677acf4ee1..898a8bf02edb 100644 --- a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java +++ b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java @@ -16,11 +16,13 @@ package android.media.tv.tuner.frontend; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.media.tv.flags.Flags; import android.media.tv.tuner.Lnb; import android.media.tv.tuner.TunerVersionChecker; @@ -61,7 +63,7 @@ public class FrontendStatus { FRONTEND_STATUS_TYPE_DVBT_CELL_IDS, FRONTEND_STATUS_TYPE_ATSC3_ALL_PLP_INFO, FRONTEND_STATUS_TYPE_IPTV_CONTENT_URL, FRONTEND_STATUS_TYPE_IPTV_PACKETS_LOST, FRONTEND_STATUS_TYPE_IPTV_PACKETS_RECEIVED, FRONTEND_STATUS_TYPE_IPTV_WORST_JITTER_MS, - FRONTEND_STATUS_TYPE_IPTV_AVERAGE_JITTER_MS}) + FRONTEND_STATUS_TYPE_IPTV_AVERAGE_JITTER_MS, FRONTEND_STATUS_TYPE_STANDARD_EXT}) @Retention(RetentionPolicy.SOURCE) public @interface FrontendStatusType {} @@ -311,6 +313,13 @@ public class FrontendStatus { public static final int FRONTEND_STATUS_TYPE_ATSC3_ALL_PLP_INFO = android.hardware.tv.tuner.FrontendStatusType.ATSC3_ALL_PLP_INFO; + /** + * Standard extension. + */ + @FlaggedApi(Flags.FLAG_TUNER_W_APIS) + public static final int FRONTEND_STATUS_TYPE_STANDARD_EXT = + android.hardware.tv.tuner.FrontendStatusType.STANDARD_EXT; + /** @hide */ @IntDef(value = { AtscFrontendSettings.MODULATION_UNDEFINED, @@ -558,6 +567,7 @@ public class FrontendStatus { private Long mIptvPacketsReceived; private Integer mIptvWorstJitterMs; private Integer mIptvAverageJitterMs; + private StandardExt mStandardExt; // Constructed and fields set by JNI code. private FrontendStatus() { @@ -1273,4 +1283,27 @@ public class FrontendStatus { } return mIptvAverageJitterMs; } + /** + * Gets the standard extension. + * + * <p>The tuner standard DVB-T has the extension DVB-T2, and the standard DVB-S has the + * extensions DVB-S2 and DVB-S2X. This method returns the current standard extension within the + * same standard series. This frontend status is reported when the standard extension + * transitions to another during playback. + * + * <p>This query is supported only by Tuner HAL 4.0 or higher. Use + * {@link TunerVersionChecker#getTunerVersion()} to check the version. + * + * @return The current standard extension. + */ + @NonNull + @FlaggedApi(Flags.FLAG_TUNER_W_APIS) + public StandardExt getStandardExt() { + TunerVersionChecker.checkHigherOrEqualVersionTo( + TunerVersionChecker.TUNER_VERSION_4_0, "StandardExt status"); + if (mStandardExt == null) { + throw new IllegalStateException("StandardExt status is empty"); + } + return mStandardExt; + } } diff --git a/media/java/android/media/tv/tuner/frontend/StandardExt.java b/media/java/android/media/tv/tuner/frontend/StandardExt.java new file mode 100644 index 000000000000..490727278b46 --- /dev/null +++ b/media/java/android/media/tv/tuner/frontend/StandardExt.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.tuner.frontend; + +import android.annotation.FlaggedApi; +import android.annotation.SystemApi; +import android.hardware.tv.tuner.FrontendDvbsStandard; +import android.hardware.tv.tuner.FrontendDvbtStandard; +import android.media.tv.flags.Flags; + +/** + * Standard extension for the standard DVB-T and DVB-S series. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_TUNER_W_APIS) +public final class StandardExt { + private final int mDvbsStandardExt; + private final int mDvbtStandardExt; + + /** + * Private constructor called by JNI only. + */ + private StandardExt(int dvbsStandardExt, int dvbtStandardExt) { + mDvbsStandardExt = dvbsStandardExt; + mDvbtStandardExt = dvbtStandardExt; + } + + /** + * Gets the DVB-S standard extension within the DVB-S standard series. + * + * @return An integer representing the standard, such as + * {@link DvbsFrontendSettings#STANDARD_S}. + * + * @see android.media.tv.tuner.frontend.DvbsFrontendSettings + */ + @DvbsFrontendSettings.Standard + public int getDvbsStandardExt() { + if (mDvbsStandardExt == FrontendDvbsStandard.UNDEFINED) { + throw new IllegalStateException("No DVB-S standard transition"); + } + return mDvbsStandardExt; + } + + /** + * Gets the DVB-T standard extension within the DVB-T standard series. + * + * @return An integer representing the standard, such as + * {@link DvbtFrontendSettings#STANDARD_T}. + * + * @see android.media.tv.tuner.frontend.DvbtFrontendSettings + */ + @DvbtFrontendSettings.Standard + public int getDvbtStandardExt() { + if (mDvbtStandardExt == FrontendDvbtStandard.UNDEFINED) { + throw new IllegalStateException("No DVB-T standard transition"); + } + return mDvbtStandardExt; + } +} diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index 9e1e2c39ee5b..80ca4f239a26 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -144,6 +144,7 @@ #include <aidl/android/hardware/tv/tuner/FrontendScanAtsc3PlpInfo.h> #include <aidl/android/hardware/tv/tuner/FrontendScanMessageStandard.h> #include <aidl/android/hardware/tv/tuner/FrontendSpectralInversion.h> +#include <aidl/android/hardware/tv/tuner/FrontendStandardExt.h> #include <aidl/android/hardware/tv/tuner/FrontendStatus.h> #include <aidl/android/hardware/tv/tuner/FrontendStatusAtsc3PlpInfo.h> #include <aidl/android/hardware/tv/tuner/FrontendStatusType.h> @@ -302,6 +303,7 @@ using ::aidl::android::hardware::tv::tuner::FrontendRollOff; using ::aidl::android::hardware::tv::tuner::FrontendScanAtsc3PlpInfo; using ::aidl::android::hardware::tv::tuner::FrontendScanMessageStandard; using ::aidl::android::hardware::tv::tuner::FrontendSpectralInversion; +using ::aidl::android::hardware::tv::tuner::FrontendStandardExt; using ::aidl::android::hardware::tv::tuner::FrontendStatus; using ::aidl::android::hardware::tv::tuner::FrontendStatusAtsc3PlpInfo; using ::aidl::android::hardware::tv::tuner::FrontendStatusType; @@ -2937,6 +2939,33 @@ jobject JTuner::getFrontendStatus(jintArray types) { env->SetObjectField(statusObj, field, newIntegerObj.get()); break; } + case FrontendStatus::Tag::standardExt: { + jfieldID field = env->GetFieldID(clazz, "mStandardExt", + "Landroid/media/tv/tuner/frontend/StandardExt;"); + ScopedLocalRef standardExtClazz(env, + env->FindClass("android/media/tv/tuner/frontend/StandardExt")); + jmethodID initStandardExt = env->GetMethodID(standardExtClazz.get(), "<init>", + "(II)V"); + + jint dvbsStandardExt = static_cast<jint>(FrontendDvbsStandard::UNDEFINED); + jint dvbtStandardExt = static_cast<jint>(FrontendDvbtStandard::UNDEFINED); + FrontendStandardExt standardExt = s.get<FrontendStatus::Tag::standardExt>(); + switch (standardExt.getTag()) { + case FrontendStandardExt::Tag::dvbsStandardExt: { + dvbsStandardExt = static_cast<jint>(standardExt + .get<FrontendStandardExt::Tag::dvbsStandardExt>()); + break; + } + case FrontendStandardExt::Tag::dvbtStandardExt: { + dvbtStandardExt = static_cast<jint>(standardExt + .get<FrontendStandardExt::Tag::dvbtStandardExt>()); + break; + } + } + ScopedLocalRef standardExtObj(env, env->NewObject(standardExtClazz.get(), + initStandardExt, dvbsStandardExt, dvbtStandardExt)); + env->SetObjectField(statusObj, field, standardExtObj.get()); + } } } return statusObj; diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index 095d7d1145ae..15f77cebf3ba 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -39,6 +39,7 @@ #include <utils/SystemClock.h> #include <chrono> +#include <future> #include <set> #include <utility> #include <vector> @@ -104,6 +105,7 @@ private: size_t mAvailableSlots GUARDED_BY(sHintMutex) = 0; bool mHalSupported = true; HalMessageQueue::MemTransaction mFmqTransaction GUARDED_BY(sHintMutex); + std::future<bool> mChannelCreationFinished; }; struct APerformanceHintManager { @@ -218,6 +220,8 @@ APerformanceHintManager::~APerformanceHintManager() { } APerformanceHintManager* APerformanceHintManager::getInstance() { + static std::once_flag creationFlag; + static APerformanceHintManager* instance = nullptr; if (gHintManagerForTesting) { return gHintManagerForTesting.get(); } @@ -226,7 +230,7 @@ APerformanceHintManager* APerformanceHintManager::getInstance() { std::shared_ptr<APerformanceHintManager>(create(*gIHintManagerForTesting)); return gHintManagerForTesting.get(); } - static APerformanceHintManager* instance = create(nullptr); + std::call_once(creationFlag, []() { instance = create(nullptr); }); return instance; } @@ -522,25 +526,28 @@ bool FMQWrapper::isSupported() { } bool FMQWrapper::startChannel(IHintManager* manager) { - if (isSupported() && !isActive()) { - std::optional<hal::ChannelConfig> config; - auto ret = manager->getSessionChannel(mToken, &config); - if (ret.isOk() && config.has_value()) { - std::scoped_lock lock{sHintMutex}; - mQueue = std::make_shared<HalMessageQueue>(config->channelDescriptor, true); - if (config->eventFlagDescriptor.has_value()) { - mFlagQueue = std::make_shared<HalFlagQueue>(*config->eventFlagDescriptor, true); - android::hardware::EventFlag::createEventFlag(mFlagQueue->getEventFlagWord(), - &mEventFlag); - mWriteMask = config->writeFlagBitmask; + if (isSupported() && !isActive() && manager->isRemote()) { + mChannelCreationFinished = std::async(std::launch::async, [&, this, manager]() { + std::optional<hal::ChannelConfig> config; + auto ret = manager->getSessionChannel(mToken, &config); + if (ret.isOk() && config.has_value()) { + std::scoped_lock lock{sHintMutex}; + mQueue = std::make_shared<HalMessageQueue>(config->channelDescriptor, true); + if (config->eventFlagDescriptor.has_value()) { + mFlagQueue = std::make_shared<HalFlagQueue>(*config->eventFlagDescriptor, true); + android::hardware::EventFlag::createEventFlag(mFlagQueue->getEventFlagWord(), + &mEventFlag); + mWriteMask = config->writeFlagBitmask; + } + updatePersistentTransaction(); + } else if (ret.isOk() && !config.has_value()) { + ALOGV("FMQ channel enabled but unsupported."); + setUnsupported(); + } else { + ALOGE("%s: FMQ channel initialization failed: %s", __FUNCTION__, ret.getMessage()); } - updatePersistentTransaction(); - } else if (ret.isOk() && !config.has_value()) { - ALOGV("FMQ channel enabled but unsupported."); - setUnsupported(); - } else { - ALOGE("%s: FMQ channel initialization failed: %s", __FUNCTION__, ret.getMessage()); - } + return true; + }); } return isActive(); } diff --git a/omapi/aidl/vts/functional/AccessControlApp/Android.bp b/omapi/aidl/vts/functional/AccessControlApp/Android.bp index f03c3f6eb647..57d75f596485 100644 --- a/omapi/aidl/vts/functional/AccessControlApp/Android.bp +++ b/omapi/aidl/vts/functional/AccessControlApp/Android.bp @@ -15,6 +15,7 @@ // package { + default_team: "trendy_team_fwk_nfc", default_applicable_licenses: ["Android-Apache-2.0"], } diff --git a/omapi/aidl/vts/functional/omapi/Android.bp b/omapi/aidl/vts/functional/omapi/Android.bp index c41479f9e9cf..8ee55ff56bb6 100644 --- a/omapi/aidl/vts/functional/omapi/Android.bp +++ b/omapi/aidl/vts/functional/omapi/Android.bp @@ -15,6 +15,7 @@ // package { + default_team: "trendy_team_fwk_nfc", default_applicable_licenses: ["Android-Apache-2.0"], } diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java index 9a8261c20f8a..8e5ae2082ac1 100644 --- a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java @@ -24,9 +24,12 @@ import static com.android.server.crashrecovery.CrashRecoveryUtils.dumpCrashRecov import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -94,6 +97,8 @@ import java.util.concurrent.TimeUnit; * be notified. * @hide */ +@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) public class PackageWatchdog { private static final String TAG = "PackageWatchdog"; @@ -351,7 +356,7 @@ public class PackageWatchdog { * * <p>If monitoring a package supporting explicit health check, at the end of the monitoring * duration if {@link #onHealthCheckPassed} was never called, - * {@link PackageHealthObserver#execute} will be called as if the package failed. + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} will be called as if the package failed. * * <p>If {@code observer} is already monitoring a package in {@code packageNames}, * the monitoring window of that package will be reset to {@code durationMs} and the health @@ -514,8 +519,8 @@ public class PackageWatchdog { maybeExecute(currentObserverToNotify, versionedPackage, failureReason, currentObserverImpact, mitigationCount); } else { - currentObserverToNotify.execute(versionedPackage, - failureReason, mitigationCount); + currentObserverToNotify.onExecuteHealthCheckMitigation( + versionedPackage, failureReason, mitigationCount); } } } @@ -550,7 +555,8 @@ public class PackageWatchdog { maybeExecute(currentObserverToNotify, failingPackage, failureReason, currentObserverImpact, /*mitigationCount=*/ 1); } else { - currentObserverToNotify.execute(failingPackage, failureReason, 1); + currentObserverToNotify.onExecuteHealthCheckMitigation(failingPackage, + failureReason, 1); } } } @@ -564,7 +570,8 @@ public class PackageWatchdog { synchronized (mLock) { mLastMitigation = mSystemClock.uptimeMillis(); } - currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount); + currentObserverToNotify.onExecuteHealthCheckMitigation(versionedPackage, failureReason, + mitigationCount); } } @@ -626,12 +633,12 @@ public class PackageWatchdog { currentObserverInternal.setBootMitigationCount( currentObserverMitigationCount); saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); - currentObserverToNotify.executeBootLoopMitigation( + currentObserverToNotify.onExecuteBootLoopMitigation( currentObserverMitigationCount); } else { mBootThreshold.setMitigationCount(mitigationCount); mBootThreshold.saveMitigationCountToMetadata(); - currentObserverToNotify.executeBootLoopMitigation(mitigationCount); + currentObserverToNotify.onExecuteBootLoopMitigation(mitigationCount); } } } @@ -717,7 +724,9 @@ public class PackageWatchdog { return mPackagesExemptFromImpactLevelThreshold; } - /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. + /** + * Possible severity values of the user impact of a + * {@link PackageHealthObserver#onExecuteHealthCheckMitigation}. * @hide */ @Retention(SOURCE) @@ -753,6 +762,7 @@ public class PackageWatchdog { } /** Register instances of this interface to receive notifications on package failure. */ + @SuppressLint({"CallbackName"}) public interface PackageHealthObserver { /** * Called when health check fails for the {@code versionedPackage}. @@ -765,7 +775,7 @@ public class PackageWatchdog { * * * @return any one of {@link PackageHealthObserverImpact} to express the impact - * to the user on {@link #execute} + * to the user on {@link #onExecuteHealthCheckMitigation} */ @PackageHealthObserverImpact int onHealthCheckFailed( @Nullable VersionedPackage versionedPackage, @@ -773,7 +783,10 @@ public class PackageWatchdog { int mitigationCount); /** - * Executes mitigation for {@link #onHealthCheckFailed}. + * This would be called after {@link #onHealthCheckFailed}. + * This is called only if current observer returned least + * {@link PackageHealthObserverImpact} mitigation for failed health + * check. * * @param versionedPackage the package that is failing. This may be null if a native * service is crashing. @@ -782,7 +795,7 @@ public class PackageWatchdog { * (including this time). * @return {@code true} if action was executed successfully, {@code false} otherwise */ - boolean execute(@Nullable VersionedPackage versionedPackage, + boolean onExecuteHealthCheckMitigation(@Nullable VersionedPackage versionedPackage, @FailureReasons int failureReason, int mitigationCount); @@ -798,11 +811,14 @@ public class PackageWatchdog { } /** - * Executes mitigation for {@link #onBootLoop} + * This would be called after {@link #onBootLoop}. + * This is called only if current observer returned least + * {@link PackageHealthObserverImpact} mitigation for fixing boot loop + * * @param mitigationCount the number of times mitigation has been attempted for this * boot loop (including this time). */ - default boolean executeBootLoopMitigation(int mitigationCount) { + default boolean onExecuteBootLoopMitigation(int mitigationCount) { return false; } @@ -1083,7 +1099,7 @@ public class PackageWatchdog { if (versionedPkg != null) { Slog.i(TAG, "Explicit health check failed for package " + versionedPkg); - registeredObserver.execute(versionedPkg, + registeredObserver.onExecuteHealthCheckMitigation(versionedPkg, PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK, 1); } } diff --git a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java index f1b2f6b38efa..f1103e19c90e 100644 --- a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java +++ b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java @@ -728,7 +728,7 @@ public class RescueParty { } @Override - public boolean execute(@Nullable VersionedPackage failedPackage, + public boolean onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, @FailureReasons int failureReason, int mitigationCount) { if (isDisabled()) { return false; @@ -796,7 +796,7 @@ public class RescueParty { } @Override - public boolean executeBootLoopMitigation(int mitigationCount) { + public boolean onExecuteBootLoopMitigation(int mitigationCount) { if (isDisabled()) { return false; } diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java index 2931652ee081..8277e573e7c2 100644 --- a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java +++ b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -19,8 +19,11 @@ package com.android.server.rollback; import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent; import android.annotation.AnyThread; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.annotation.WorkerThread; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -75,6 +78,9 @@ import java.util.function.Consumer; * * @hide */ +@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY) +@SuppressLint({"CallbackName"}) +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) public final class RollbackPackageHealthObserver implements PackageHealthObserver { private static final String TAG = "RollbackPackageHealthObserver"; private static final String NAME = "rollback-observer"; @@ -148,7 +154,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve // Note: For non-native crashes the rollback-all step has higher impact impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; } else if (getAvailableRollback(failedPackage) != null) { - // Rollback is available, we may get a callback into #execute + // Rollback is available, we may get a callback into #onExecuteHealthCheckMitigation impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; } else if (anyRollbackAvailable) { // If any rollbacks are available, we will commit them @@ -165,7 +171,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve } @Override - public boolean execute(@Nullable VersionedPackage failedPackage, + public boolean onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage, @FailureReasons int rollbackReason, int mitigationCount) { Slog.i(TAG, "Executing remediation." + " failedPackage: " @@ -219,7 +225,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve } @Override - public boolean executeBootLoopMitigation(int mitigationCount) { + public boolean onExecuteBootLoopMitigation(int mitigationCount) { if (Flags.recoverabilityDetection()) { List<RollbackInfo> availableRollbacks = getAvailableRollbacks(); diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java index bfc00bb8b94d..b48c55ddfef0 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java @@ -329,18 +329,6 @@ public final class RemotePrintDocument { disconnectFromRemoteDocument(); } - public void kill(String reason) { - if (DEBUG) { - Log.i(LOG_TAG, "[CALLED] kill()"); - } - - try { - mPrintDocumentAdapter.kill(reason); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error calling kill()", re); - } - } - public boolean isUpdating() { return mState == STATE_UPDATING || mState == STATE_CANCELING; } diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index c4173ed999f3..bd2b5ec8436e 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -514,8 +514,12 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat ensureErrorUiShown(null, PrintErrorFragment.ACTION_RETRY); setState(STATE_UPDATE_FAILED); - - mPrintedDocument.kill(message); + if (DEBUG) { + Log.i(LOG_TAG, "PrintJob state[" + PrintJobInfo.STATE_FAILED + "] reason: " + message); + } + PrintSpoolerService spooler = mSpoolerProvider.getSpooler(); + spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_FAILED, message); + mPrintedDocument.finish(); } @Override diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml index 18696c627ec6..91a95a51fae2 100644 --- a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml +++ b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml @@ -16,6 +16,12 @@ --> <ripple xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:color="?android:attr/colorControlHighlight"> - <item android:drawable="@drawable/audio_sharing_rounded_bg"/> + <item> + <shape android:shape="rectangle"> + <corners android:radius="4dp" /> + <solid android:color="?androidprv:attr/colorAccentPrimary" /> + </shape> + </item> </ripple>
\ No newline at end of file diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_bottom.xml index 35517ea0ec11..cce8a75a3385 100644 --- a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg.xml +++ b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_bottom.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2023 The Android Open Source Project + ~ 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. @@ -15,10 +15,17 @@ ~ limitations under the License. --> -<shape - xmlns:android="http://schemas.android.com/apk/res/android" +<ripple xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorAccentPrimary" /> - <corners android:radius="12dp" /> -</shape>
\ No newline at end of file + android:color="?android:attr/colorControlHighlight"> + <item> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="12dp" + android:bottomRightRadius="12dp" + android:topLeftRadius="4dp" + android:topRightRadius="4dp" /> + <solid android:color="?androidprv:attr/colorAccentPrimary" /> + </shape> + </item> +</ripple>
\ No newline at end of file diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_top.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_top.xml new file mode 100644 index 000000000000..140419705dd5 --- /dev/null +++ b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_top.xml @@ -0,0 +1,31 @@ +<?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. + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:color="?android:attr/colorControlHighlight"> + <item> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="4dp" + android:bottomRightRadius="4dp" + android:topLeftRadius="12dp" + android:topRightRadius="12dp" /> + <solid android:color="?androidprv:attr/colorAccentPrimary" /> + </shape> + </item> +</ripple>
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt index a33fcc6747b4..c16366e14560 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt @@ -279,7 +279,7 @@ class DeviceSettingServiceConnection( getService(intent, IDeviceSettingsProviderService.Stub::asInterface) .stateIn( coroutineScope.plus(backgroundCoroutineContext), - SharingStarted.WhileSubscribed(), + SharingStarted.WhileSubscribed(stopTimeoutMillis = SERVICE_CONNECTION_STOP_MILLIS), ServiceConnectionStatus.Connecting, ) }, @@ -370,5 +370,6 @@ class DeviceSettingServiceConnection( const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME" const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" + const val SERVICE_CONNECTION_STOP_MILLIS = 1000L } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java index 63661f698f4e..4f315a2a2486 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java @@ -74,23 +74,7 @@ public final class InputRouteManager { new AudioDeviceCallback() { @Override public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) { - // Activate the last hot plugged valid input device, to match the output device - // behavior. - @AudioDeviceType int deviceTypeToActivate = mSelectedInputDeviceType; - for (AudioDeviceInfo info : addedDevices) { - if (InputMediaDevice.isSupportedInputDevice(info.getType())) { - deviceTypeToActivate = info.getType(); - } - } - - // Only activate if we find a different valid input device. e.g. if none of the - // addedDevices is supported input device, we don't need to activate anything. - if (mSelectedInputDeviceType != deviceTypeToActivate) { - mSelectedInputDeviceType = deviceTypeToActivate; - AudioDeviceAttributes deviceAttributes = - createInputDeviceAttributes(mSelectedInputDeviceType); - setPreferredDeviceForAllPresets(deviceAttributes); - } + applyDefaultSelectedTypeToAllPresets(); } @Override diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java index d808a25ebc04..782cee23fb42 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java @@ -24,7 +24,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -139,18 +138,6 @@ public class InputRouteManagerTest { /* address= */ ""); } - private AudioDeviceAttributes getUsbHeadsetDeviceAttributes() { - return new AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_INPUT, - AudioDeviceInfo.TYPE_USB_HEADSET, - /* address= */ ""); - } - - private AudioDeviceAttributes getHdmiDeviceAttributes() { - return new AudioDeviceAttributes( - AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_HDMI, /* address= */ ""); - } - private void onPreferredDevicesForCapturePresetChanged(InputRouteManager inputRouteManager) { final List<AudioDeviceAttributes> audioDeviceAttributesList = new ArrayList<AudioDeviceAttributes>(); @@ -316,47 +303,21 @@ public class InputRouteManagerTest { } @Test - public void onAudioDevicesAdded_shouldActivateAddedDevice() { - final AudioManager audioManager = mock(AudioManager.class); - InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager); - AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()}; - inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices); - - // The only added wired headset will be activated. - for (@MediaRecorder.Source int preset : PRESETS) { - verify(audioManager, atLeast(1)) - .setPreferredDeviceForCapturePreset(preset, getWiredHeadsetDeviceAttributes()); - } - } - - @Test - public void onAudioDevicesAdded_shouldActivateLastAddedDevice() { + public void onAudioDevicesAdded_shouldApplyDefaultSelectedDeviceToAllPresets() { final AudioManager audioManager = mock(AudioManager.class); - InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager); - AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockUsbHeadsetInfo()}; - inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices); - - // When adding multiple valid input devices, the last added device (usb headset in this - // case) will be activated. - for (@MediaRecorder.Source int preset : PRESETS) { - verify(audioManager, never()) - .setPreferredDeviceForCapturePreset(preset, getWiredHeadsetDeviceAttributes()); - verify(audioManager, atLeast(1)) - .setPreferredDeviceForCapturePreset(preset, getUsbHeadsetDeviceAttributes()); - } - } + AudioDeviceAttributes wiredHeadsetDeviceAttributes = getWiredHeadsetDeviceAttributes(); + when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES)) + .thenReturn(Collections.singletonList(wiredHeadsetDeviceAttributes)); - @Test - public void onAudioDevicesAdded_doNotActivateInvalidAddedDevice() { - final AudioManager audioManager = mock(AudioManager.class); InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager); - AudioDeviceInfo[] devices = {mockHdmiInfo()}; + AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()}; inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices); - // Do not activate since HDMI is not a valid input device. + // Called twice, one after initiation, the other after onAudioDevicesAdded call. + verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES); for (@MediaRecorder.Source int preset : PRESETS) { - verify(audioManager, never()) - .setPreferredDeviceForCapturePreset(preset, getHdmiDeviceAttributes()); + verify(audioManager, atLeast(2)) + .setPreferredDeviceForCapturePreset(preset, wiredHeadsetDeviceAttributes); } } diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index d39b5645109d..1659c9eb67f2 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -903,8 +903,8 @@ public class SettingsBackupTest { Settings.System.EGG_MODE, // I am the lolrus Settings.System.END_BUTTON_BEHAVIOR, // bug? Settings.System.DEFAULT_DEVICE_FONT_SCALE, // Non configurable - Settings.System - .HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, + Settings.System.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY, + Settings.System.INPUT_GAIN_INDEX_SETTINGS, // candidate for backup? Settings.System.LOCKSCREEN_DISABLED, // ? Settings.System.MEDIA_BUTTON_RECEIVER, // candidate for backup? diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index bcfd8f620f9c..0a68e6791df5 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -90,10 +90,10 @@ import com.android.internal.app.ChooserActivity; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import libcore.io.Streams; - import com.google.android.collect.Lists; +import libcore.io.Streams; + import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.File; @@ -171,8 +171,8 @@ public class BugreportProgressService extends Service { static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT"; static final String EXTRA_INFO = "android.intent.extra.INFO"; - static final String EXTRA_EXTRA_ATTACHMENT_URI = - "android.intent.extra.EXTRA_ATTACHMENT_URI"; + static final String EXTRA_EXTRA_ATTACHMENT_URIS = + "android.intent.extra.EXTRA_ATTACHMENT_URIS"; private static final int MSG_SERVICE_COMMAND = 1; private static final int MSG_DELAYED_SCREENSHOT = 2; @@ -682,10 +682,11 @@ public class BugreportProgressService extends Service { long nonce = intent.getLongExtra(EXTRA_BUGREPORT_NONCE, 0); String baseName = getBugreportBaseName(bugreportType); String name = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); - Uri extraAttachment = intent.getParcelableExtra(EXTRA_EXTRA_ATTACHMENT_URI, Uri.class); + List<Uri> extraAttachments = + intent.getParcelableArrayListExtra(EXTRA_EXTRA_ATTACHMENT_URIS, Uri.class); BugreportInfo info = new BugreportInfo(mContext, baseName, name, shareTitle, - shareDescription, bugreportType, mBugreportsDir, nonce, extraAttachment); + shareDescription, bugreportType, mBugreportsDir, nonce, extraAttachments); synchronized (mLock) { if (info.bugreportFile.exists()) { Log.e(TAG, "Failed to start bugreport generation, the requested bugreport file " @@ -1233,9 +1234,13 @@ public class BugreportProgressService extends Service { clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); attachments.add(screenshotUri); } - if (info.extraAttachment != null) { - clipData.addItem(new ClipData.Item(null, null, null, info.extraAttachment)); - attachments.add(info.extraAttachment); + if (info.extraAttachments != null) { + info.extraAttachments.forEach(it -> { + if (it != null) { + clipData.addItem(new ClipData.Item(null, null, null, it)); + attachments.add(it); + } + }); } intent.setClipData(clipData); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); @@ -2096,7 +2101,7 @@ public class BugreportProgressService extends Service { final long nonce; @Nullable - public Uri extraAttachment = null; + public List<Uri> extraAttachments = null; private final Object mLock = new Object(); @@ -2106,7 +2111,7 @@ public class BugreportProgressService extends Service { BugreportInfo(Context context, String baseName, String name, @Nullable String shareTitle, @Nullable String shareDescription, @BugreportParams.BugreportMode int type, File bugreportsDir, long nonce, - @Nullable Uri extraAttachment) { + @Nullable List<Uri> extraAttachments) { this.context = context; this.name = this.initialName = name; this.shareTitle = shareTitle == null ? "" : shareTitle; @@ -2115,7 +2120,7 @@ public class BugreportProgressService extends Service { this.nonce = nonce; this.baseName = baseName; this.bugreportFile = new File(bugreportsDir, getFileName(this, ".zip")); - this.extraAttachment = extraAttachment; + this.extraAttachments = extraAttachments; } void createBugreportFile() { diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 1c29db128a8c..7b8dddb557a9 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1529,6 +1529,13 @@ flag { } flag { + name: "shade_window_goes_around" + namespace: "systemui" + description: "Enables the shade window to move between displays" + bug: "362719719" +} + +flag { name: "media_projection_request_attribution_fix" namespace: "systemui" description: "Ensure MediaProjection consent requests are properly attributed" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt new file mode 100644 index 000000000000..cd2dd04568a7 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt @@ -0,0 +1,166 @@ +/* + * 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.animation.back + +import android.util.TimeUtils +import android.view.Choreographer +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_MOVE +import android.view.VelocityTracker +import android.view.animation.Interpolator +import android.window.BackEvent +import android.window.OnBackAnimationCallback +import com.android.app.animation.Interpolators +import com.android.internal.dynamicanimation.animation.DynamicAnimation +import com.android.internal.dynamicanimation.animation.FlingAnimation +import com.android.internal.dynamicanimation.animation.FloatValueHolder +import com.android.window.flags.Flags.predictiveBackTimestampApi + +private const val FLING_FRICTION = 6f +private const val SCALE_FACTOR = 100f + +/** + * Enhanced [OnBackAnimationCallback] with automatic fling animation and interpolated progress. + * + * Simplifies back gesture handling by animating flings and emitting processed events through + * `compat` functions. Customize progress interpolation with an optional [Interpolator]. + * + * @param progressInterpolator [Interpolator] for progress, defaults to + * [Interpolators.BACK_GESTURE]. + */ +abstract class FlingOnBackAnimationCallback( + val progressInterpolator: Interpolator = Interpolators.BACK_GESTURE +) : OnBackAnimationCallback { + + private val velocityTracker = VelocityTracker.obtain() + private var lastBackEvent: BackEvent? = null + private var downTime: Long? = null + + private var backInvokedFlingAnim: FlingAnimation? = null + private val backInvokedFlingUpdateListener = + DynamicAnimation.OnAnimationUpdateListener { _, progress: Float, _ -> + lastBackEvent?.let { + val backEvent = + BackEvent( + it.touchX, + it.touchY, + progress / SCALE_FACTOR, + it.swipeEdge, + it.frameTimeMillis, + ) + onBackProgressedCompat(backEvent) + } + } + private val backInvokedFlingEndListener = + DynamicAnimation.OnAnimationEndListener { _, _, _, _ -> + onBackInvokedCompat() + reset() + } + + abstract fun onBackStartedCompat(backEvent: BackEvent) + + abstract fun onBackProgressedCompat(backEvent: BackEvent) + + abstract fun onBackInvokedCompat() + + abstract fun onBackCancelledCompat() + + final override fun onBackStarted(backEvent: BackEvent) { + if (backInvokedFlingAnim != null) { + // This should never happen but let's call onBackInvokedCompat() just in case there is + // still a fling animation in progress + onBackInvokedCompat() + } + reset() + if (predictiveBackTimestampApi()) { + downTime = backEvent.frameTimeMillis + } + onBackStartedCompat(backEvent) + } + + final override fun onBackProgressed(backEvent: BackEvent) { + val interpolatedProgress = progressInterpolator.getInterpolation(backEvent.progress) + if (predictiveBackTimestampApi()) { + velocityTracker.addMovement( + MotionEvent.obtain( + /* downTime */ downTime!!, + /* eventTime */ backEvent.frameTimeMillis, + /* action */ ACTION_MOVE, + /* x */ interpolatedProgress * SCALE_FACTOR, + /* y */ 0f, + /* metaState */ 0, + ) + ) + lastBackEvent = + BackEvent( + backEvent.touchX, + backEvent.touchY, + interpolatedProgress, + backEvent.swipeEdge, + backEvent.frameTimeMillis, + ) + } else { + lastBackEvent = + BackEvent( + backEvent.touchX, + backEvent.touchY, + interpolatedProgress, + backEvent.swipeEdge, + ) + } + lastBackEvent?.let { onBackProgressedCompat(it) } + } + + final override fun onBackInvoked() { + if (predictiveBackTimestampApi() && lastBackEvent != null) { + velocityTracker.computeCurrentVelocity(1000) + backInvokedFlingAnim = + FlingAnimation(FloatValueHolder()) + .setStartValue((lastBackEvent?.progress ?: 0f) * SCALE_FACTOR) + .setFriction(FLING_FRICTION) + .setStartVelocity(velocityTracker.xVelocity) + .setMinValue(0f) + .setMaxValue(SCALE_FACTOR) + .also { + it.addUpdateListener(backInvokedFlingUpdateListener) + it.addEndListener(backInvokedFlingEndListener) + it.start() + // do an animation-frame immediately to prevent idle frame + it.doAnimationFrame( + Choreographer.getInstance().lastFrameTimeNanos / TimeUtils.NANOS_PER_MS + ) + } + } else { + onBackInvokedCompat() + reset() + } + } + + final override fun onBackCancelled() { + onBackCancelledCompat() + reset() + } + + private fun reset() { + velocityTracker.clear() + backInvokedFlingAnim?.removeEndListener(backInvokedFlingEndListener) + backInvokedFlingAnim?.removeUpdateListener(backInvokedFlingUpdateListener) + lastBackEvent = null + backInvokedFlingAnim = null + downTime = null + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt index 8740d1467296..f708de3e9234 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt @@ -23,6 +23,7 @@ import android.window.BackEvent import android.window.OnBackAnimationCallback import android.window.OnBackInvokedDispatcher import android.window.OnBackInvokedDispatcher.Priority +import com.android.app.animation.Interpolators /** * Generates an [OnBackAnimationCallback] given a [backAnimationSpec]. [onBackProgressed] will be @@ -40,16 +41,16 @@ fun onBackAnimationCallbackFrom( onBackInvoked: () -> Unit = {}, onBackCancelled: () -> Unit = {}, ): OnBackAnimationCallback { - return object : OnBackAnimationCallback { + return object : FlingOnBackAnimationCallback(progressInterpolator = Interpolators.LINEAR) { private var initialY = 0f private val lastTransformation = BackTransformation() - override fun onBackStarted(backEvent: BackEvent) { + override fun onBackStartedCompat(backEvent: BackEvent) { initialY = backEvent.touchY onBackStarted(backEvent) } - override fun onBackProgressed(backEvent: BackEvent) { + override fun onBackProgressedCompat(backEvent: BackEvent) { val progressY = (backEvent.touchY - initialY) / displayMetrics.heightPixels backAnimationSpec.getBackTransformation( @@ -61,11 +62,11 @@ fun onBackAnimationCallbackFrom( onBackProgressed(lastTransformation) } - override fun onBackInvoked() { + override fun onBackInvokedCompat() { onBackInvoked() } - override fun onBackCancelled() { + override fun onBackCancelledCompat() { onBackCancelled() } } @@ -86,7 +87,7 @@ fun View.registerOnBackInvokedCallbackOnViewAttached( override fun onViewAttachedToWindow(v: View) { onBackInvokedDispatcher.registerOnBackInvokedCallback( priority, - onBackAnimationCallback + onBackAnimationCallback, ) } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt index c95d12032d0a..ae75e6c089ca 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Expandable.kt @@ -74,6 +74,8 @@ import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.findViewTreeSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.android.systemui.animation.Expandable import com.android.systemui.animation.TransitionAnimator import kotlin.math.max @@ -173,9 +175,7 @@ fun Expandable( val wrappedContent = remember(content) { movableContentOf { expandable: Expandable -> - CompositionLocalProvider( - LocalContentColor provides contentColor, - ) { + CompositionLocalProvider(LocalContentColor provides contentColor) { // We make sure that the content itself (wrapped by the background) is at least // 40.dp, which is the same as the M3 buttons. This applies even if onClick is // null, to make it easier to write expandables that are sometimes clickable and @@ -253,7 +253,7 @@ fun Expandable( modifier .updateExpandableSize() .then(minInteractiveSizeModifier) - .drawWithContent { /* Don't draw anything when the dialog is shown. */} + .drawWithContent { /* Don't draw anything when the dialog is shown. */ } .onGloballyPositioned { controller.boundsInComposeViewRoot.value = it.boundsInRoot() } @@ -288,7 +288,7 @@ fun Expandable( .border(controller) .onGloballyPositioned { controller.boundsInComposeViewRoot.value = it.boundsInRoot() - }, + } ) { wrappedContent(controller.expandable) } @@ -365,19 +365,14 @@ private fun AnimatedContentInOverlay( } // Set the owners. - val overlayViewGroup = - getOverlayViewGroup( - context, - overlay, - ) + val overlayViewGroup = getOverlayViewGroup(context, overlay) overlayViewGroup.setViewTreeLifecycleOwner(composeViewRoot.findViewTreeLifecycleOwner()) overlayViewGroup.setViewTreeViewModelStoreOwner( composeViewRoot.findViewTreeViewModelStoreOwner() ) - ViewTreeSavedStateRegistryOwner.set( - overlayViewGroup, - ViewTreeSavedStateRegistryOwner.get(composeViewRoot), + overlayViewGroup.setViewTreeSavedStateRegistryOwner( + composeViewRoot.findViewTreeSavedStateRegistryOwner() ) composeView.setParentCompositionContext(compositionContext) @@ -405,10 +400,7 @@ private fun AnimatedContentInOverlay( } } -internal fun measureAndLayoutComposeViewInOverlay( - view: View, - state: TransitionAnimator.State, -) { +internal fun measureAndLayoutComposeViewInOverlay(view: View, state: TransitionAnimator.State) { val exactWidth = state.width val exactHeight = state.height view.measure( @@ -474,7 +466,7 @@ private fun ContentDrawScope.drawBackground( topLeft = Offset(halfStroke, halfStroke), size = Size(size.width - strokeWidth, size.height - strokeWidth), cornerRadius = cornerRadius.shrink(halfStroke), - style = borderStroke + style = borderStroke, ) } } else { @@ -494,11 +486,7 @@ private fun ContentDrawScope.drawBackground( if (border != null) { // Copied from androidx.compose.foundation.Border.kt. val strokeWidth = border.width.toPx() - val path = - createRoundRectPath( - (outline as Outline.Rounded).roundRect, - strokeWidth, - ) + val path = createRoundRectPath((outline as Outline.Rounded).roundRect, strokeWidth) drawPath(path, border.brush) } @@ -510,10 +498,7 @@ private fun ContentDrawScope.drawBackground( * * Copied from androidx.compose.foundation.Border.kt. */ -private fun createRoundRectPath( - roundedRect: RoundRect, - strokeWidth: Float, -): Path { +private fun createRoundRectPath(roundedRect: RoundRect, strokeWidth: Float): Path { return Path().apply { addRoundRect(roundedRect) val insetPath = @@ -532,7 +517,7 @@ private fun createInsetRoundedRect(widthPx: Float, roundedRect: RoundRect) = topLeftCornerRadius = roundedRect.topLeftCornerRadius.shrink(widthPx), topRightCornerRadius = roundedRect.topRightCornerRadius.shrink(widthPx), bottomLeftCornerRadius = roundedRect.bottomLeftCornerRadius.shrink(widthPx), - bottomRightCornerRadius = roundedRect.bottomRightCornerRadius.shrink(widthPx) + bottomRightCornerRadius = roundedRect.bottomRightCornerRadius.shrink(widthPx), ) /** diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ViewTreeSavedStateRegistryOwner.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ViewTreeSavedStateRegistryOwner.kt deleted file mode 100644 index cdc9a521b8c7..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ViewTreeSavedStateRegistryOwner.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.compose.animation - -import android.view.View -import androidx.savedstate.SavedStateRegistryOwner -import androidx.savedstate.findViewTreeSavedStateRegistryOwner -import androidx.savedstate.setViewTreeSavedStateRegistryOwner - -// TODO(b/262222023): Remove this workaround and import the new savedstate libraries in tm-qpr-dev -// instead. -object ViewTreeSavedStateRegistryOwner { - fun set(view: View, owner: SavedStateRegistryOwner?) { - view.setViewTreeSavedStateRegistryOwner(owner) - } - - fun get(view: View): SavedStateRegistryOwner? { - return view.findViewTreeSavedStateRegistryOwner() - } -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/grid/Grids.kt b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt index f49939ba3a2d..f49939ba3a2d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/grid/Grids.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt new file mode 100644 index 000000000000..311519122312 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt @@ -0,0 +1,199 @@ +/* + * 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.compose.ui.graphics + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.modifier.ModifierLocalModifierNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.requireDensity +import androidx.compose.ui.node.requireGraphicsContext +import androidx.compose.ui.node.requireLayoutCoordinates +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.util.fastForEach + +/** + * Define this as a container into which other composables can be drawn using [drawInContainer]. + * + * The elements redirected to this container will be drawn above the content of this composable. + */ +fun Modifier.container(state: ContainerState): Modifier { + return layout { measurable, constraints -> + val p = measurable.measure(constraints) + layout(p.width, p.height) { + val coords = coordinates + if (coords != null && !isLookingAhead) { + state.lastCoords = coords + } + + p.place(0, 0) + } + } + .drawWithContent { + drawContent() + state.drawInOverlay(this) + } +} + +/** + * Draw this composable into the container associated to [state]. + * + * @param state the state of the container into which we should draw this composable. + * @param enabled whether the redirection of the drawing to the container is enabled. + * @param zIndex the z-index of the composable in the container. + * @param clipPath the clip path applied when drawing this composable into the container. + */ +fun Modifier.drawInContainer( + state: ContainerState, + enabled: () -> Boolean = { true }, + zIndex: Float = 0f, + clipPath: (LayoutDirection, Density) -> Path? = { _, _ -> null }, +): Modifier { + return this.then( + DrawInContainerElement( + state = state, + enabled = enabled, + zIndex = zIndex, + clipPath = clipPath, + ) + ) +} + +class ContainerState { + private var renderers = mutableStateListOf<LayerRenderer>() + internal var lastCoords: LayoutCoordinates? = null + + internal fun onLayerRendererAttached(renderer: LayerRenderer) { + renderers.add(renderer) + renderers.sortBy { it.zIndex } + } + + internal fun onLayerRendererDetached(renderer: LayerRenderer) { + renderers.remove(renderer) + } + + internal fun drawInOverlay(drawScope: DrawScope) { + renderers.fastForEach { it.drawInOverlay(drawScope) } + } +} + +internal interface LayerRenderer { + val zIndex: Float + + fun drawInOverlay(drawScope: DrawScope) +} + +private data class DrawInContainerElement( + var state: ContainerState, + var enabled: () -> Boolean, + val zIndex: Float, + val clipPath: (LayoutDirection, Density) -> Path?, +) : ModifierNodeElement<DrawInContainerNode>() { + override fun create(): DrawInContainerNode { + return DrawInContainerNode(state, enabled, zIndex, clipPath) + } + + override fun update(node: DrawInContainerNode) { + node.state = state + node.enabled = enabled + node.zIndex = zIndex + node.clipPath = clipPath + } +} + +/** + * The implementation of [drawInContainer]. + * + * Note: this was forked from AndroidX RenderInTransitionOverlayNodeElement.kt + * (http://shortn/_3dfSFPbm8f). + */ +internal class DrawInContainerNode( + var state: ContainerState, + var enabled: () -> Boolean = { true }, + zIndex: Float = 0f, + var clipPath: (LayoutDirection, Density) -> Path? = { _, _ -> null }, +) : Modifier.Node(), DrawModifierNode, ModifierLocalModifierNode { + var zIndex by mutableFloatStateOf(zIndex) + + private inner class LayerWithRenderer(val layer: GraphicsLayer) : LayerRenderer { + override val zIndex: Float + get() = this@DrawInContainerNode.zIndex + + override fun drawInOverlay(drawScope: DrawScope) { + if (enabled()) { + with(drawScope) { + val containerCoords = + checkNotNull(state.lastCoords) { "container is not placed" } + val (x, y) = + requireLayoutCoordinates().positionInWindow() - + containerCoords.positionInWindow() + val clipPath = clipPath(layoutDirection, requireDensity()) + if (clipPath != null) { + clipPath(clipPath) { translate(x, y) { drawLayer(layer) } } + } else { + translate(x, y) { drawLayer(layer) } + } + } + } + } + } + + // Render in-place logic. Depending on the result of `renderInOverlay()`, the content will + // either render in-place or in the overlay, but never in both places. + override fun ContentDrawScope.draw() { + val layer = requireNotNull(layer) { "Error: layer never initialized" } + layer.record { this@draw.drawContent() } + if (!enabled()) { + drawLayer(layer) + } + } + + val layer: GraphicsLayer? + get() = layerWithRenderer?.layer + + private var layerWithRenderer: LayerWithRenderer? = null + + override fun onAttach() { + LayerWithRenderer(requireGraphicsContext().createGraphicsLayer()).let { + state.onLayerRendererAttached(it) + layerWithRenderer = it + } + } + + override fun onDetach() { + layerWithRenderer?.let { + state.onLayerRendererDetached(it) + requireGraphicsContext().releaseGraphicsLayer(it.layer) + } + } +} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt new file mode 100644 index 000000000000..f5c3a834a8d7 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt @@ -0,0 +1,79 @@ +/* + * 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.compose.ui.graphics + +import android.view.View +import android.view.ViewGroupOverlay +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCompositionContext +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.IntSize +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.findViewTreeSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner + +/** + * Draw this composable in the [overlay][ViewGroupOverlay] of the [current ComposeView][LocalView]. + */ +@Composable +fun Modifier.drawInOverlay(): Modifier { + val containerState = remember { ContainerState() } + val context = LocalContext.current + val localView = LocalView.current + val compositionContext = rememberCompositionContext() + val displayMetrics = context.resources.displayMetrics + val displaySize = IntSize(displayMetrics.widthPixels, displayMetrics.heightPixels) + + DisposableEffect(containerState, context, localView, compositionContext, displaySize) { + val overlay = localView.rootView.overlay as ViewGroupOverlay + val view = + ComposeView(context).apply { + setParentCompositionContext(compositionContext) + + // Set the owners. + setViewTreeLifecycleOwner(localView.findViewTreeLifecycleOwner()) + setViewTreeViewModelStoreOwner(localView.findViewTreeViewModelStoreOwner()) + setViewTreeSavedStateRegistryOwner(localView.findViewTreeSavedStateRegistryOwner()) + + setContent { Box(Modifier.fillMaxSize().container(containerState)) } + } + + overlay.add(view) + + // Make the ComposeView as big as the display. We have to manually measure and layout the + // View given that there is no layout pass in Android overlays. + view.measure( + View.MeasureSpec.makeSafeMeasureSpec(displaySize.width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeSafeMeasureSpec(displaySize.height, View.MeasureSpec.EXACTLY), + ) + view.layout(0, 0, displaySize.width, displaySize.height) + + onDispose { overlay.remove(view) } + } + + return this.drawInContainer(containerState, enabled = { true }) +} diff --git a/packages/SystemUI/compose/scene/Android.bp b/packages/SystemUI/compose/scene/Android.bp index af1172bddfc8..682c49cfd19c 100644 --- a/packages/SystemUI/compose/scene/Android.bp +++ b/packages/SystemUI/compose/scene/Android.bp @@ -40,6 +40,8 @@ android_library { static_libs: [ "androidx.compose.runtime_runtime", "androidx.compose.material3_material3", + + "PlatformComposeCore", ], kotlincflags: ["-Xjvm-default=all"], diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index ebe1df4bf55f..f14622fe7b65 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -48,10 +48,13 @@ import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastForEachReversed import androidx.compose.ui.util.lerp +import com.android.compose.animation.scene.Element.State import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.SharedElementTransformation +import com.android.compose.modifiers.thenIf +import com.android.compose.ui.graphics.drawInContainer import com.android.compose.ui.util.lerp import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -146,10 +149,58 @@ internal fun Modifier.element( // TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once // we can ensure that SceneTransitionLayoutImpl will compose new contents first. val currentTransitionStates = layoutImpl.state.transitionStates - return then(ElementModifier(layoutImpl, currentTransitionStates, content, key)) + return thenIf(layoutImpl.state.isElevationPossible(content.key, key)) { + Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates) + } + .then(ElementModifier(layoutImpl, currentTransitionStates, content, key)) .testTag(key.testTag) } +private fun Modifier.maybeElevateInContent( + layoutImpl: SceneTransitionLayoutImpl, + content: Content, + key: ElementKey, + transitionStates: List<TransitionState>, +): Modifier { + fun isSharedElement( + stateByContent: Map<ContentKey, State>, + transition: TransitionState.Transition, + ): Boolean { + fun inFromContent() = transition.fromContent in stateByContent + fun inToContent() = transition.toContent in stateByContent + fun inCurrentScene() = transition.currentScene in stateByContent + + return if (transition is TransitionState.Transition.ReplaceOverlay) { + (inFromContent() && (inToContent() || inCurrentScene())) || + (inToContent() && inCurrentScene()) + } else { + inFromContent() && inToContent() + } + } + + return drawInContainer( + content.containerState, + enabled = { + val stateByContent = layoutImpl.elements.getValue(key).stateByContent + val state = elementState(transitionStates, isInContent = { it in stateByContent }) + + state is TransitionState.Transition && + state.transformationSpec + .transformations(key, content.key) + .shared + ?.elevateInContent == content.key && + isSharedElement(stateByContent, state) && + isSharedElementEnabled(key, state) && + shouldPlaceElement( + layoutImpl, + content.key, + layoutImpl.elements.getValue(key), + state, + ) + }, + ) +} + /** * An element associated to [ElementNode]. Note that this element does not support updates as its * arguments should always be the same. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index a9a8668bd304..e1e2411da080 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -19,13 +19,16 @@ package com.android.compose.animation.scene import android.util.Log import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.util.fastAll +import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastForEach import com.android.compose.animation.scene.content.state.TransitionState +import com.android.compose.animation.scene.transformation.SharedElementTransformation import com.android.compose.animation.scene.transition.link.LinkedTransition import com.android.compose.animation.scene.transition.link.StateLink import kotlin.math.absoluteValue @@ -271,6 +274,14 @@ internal class MutableSceneTransitionLayoutStateImpl( mutableStateOf(listOf(TransitionState.Idle(initialScene, initialOverlays))) private set + /** + * The flattened list of [SharedElementTransformation] within all the transitions in + * [transitionStates]. + */ + private val transformationsWithElevation: List<SharedElementTransformation> by derivedStateOf { + transformationsWithElevation(transitionStates) + } + override val currentScene: SceneKey get() = transitionState.currentScene @@ -743,6 +754,42 @@ internal class MutableSceneTransitionLayoutStateImpl( animate() } + + private fun transformationsWithElevation( + transitionStates: List<TransitionState> + ): List<SharedElementTransformation> { + return buildList { + transitionStates.fastForEach { state -> + if (state !is TransitionState.Transition) { + return@fastForEach + } + + state.transformationSpec.transformations.fastForEach { transformation -> + if ( + transformation is SharedElementTransformation && + transformation.elevateInContent != null + ) { + add(transformation) + } + } + } + } + } + + /** + * Return whether we might need to elevate [element] (or any element if [element] is `null`) in + * [content]. + * + * This is used to compose `Modifier.container()` and `Modifier.drawInContainer()` only when + * necessary, for performance. + */ + internal fun isElevationPossible(content: ContentKey, element: ElementKey?): Boolean { + if (transformationsWithElevation.isEmpty()) return false + return transformationsWithElevation.fastAny { transformation -> + transformation.elevateInContent == content && + (element == null || transformation.matcher.matches(element, content)) + } + } } private const val TAG = "SceneTransitionLayoutState" diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index e825c6e271ed..dc26b6b382b4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -204,8 +204,17 @@ interface TransitionBuilder : BaseTransitionBuilder { * * @param enabled whether the matched element(s) should actually be shared in this transition. * Defaults to true. + * @param elevateInContent the content in which we should elevate the element when it is shared, + * drawing above all other composables of that content. If `null` (the default), we will + * simply draw this element in its original location. If not `null`, it has to be either the + * [fromContent][TransitionState.Transition.fromContent] or + * [toContent][TransitionState.Transition.toContent] of the transition. */ - fun sharedElement(matcher: ElementMatcher, enabled: Boolean = true) + fun sharedElement( + matcher: ElementMatcher, + enabled: Boolean = true, + elevateInContent: ContentKey? = null, + ) /** * Adds the transformations in [builder] but in reversed order. This allows you to partially diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index a5ad999f7a64..269d91b02e7d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -249,8 +249,22 @@ internal class TransitionBuilderImpl(override val transition: TransitionState.Tr reversed = false } - override fun sharedElement(matcher: ElementMatcher, enabled: Boolean) { - transformations.add(SharedElementTransformation(matcher, enabled)) + override fun sharedElement( + matcher: ElementMatcher, + enabled: Boolean, + elevateInContent: ContentKey?, + ) { + check( + elevateInContent == null || + elevateInContent == transition.fromContent || + elevateInContent == transition.toContent + ) { + "elevateInContent (${elevateInContent?.debugName}) should be either fromContent " + + "(${transition.fromContent.debugName}) or toContent " + + "(${transition.toContent.debugName})" + } + + transformations.add(SharedElementTransformation(matcher, enabled, elevateInContent)) } override fun timestampRange( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index c8407b13db66..8187e3932975 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -51,6 +51,9 @@ import com.android.compose.animation.scene.animateSharedValueAsState import com.android.compose.animation.scene.element import com.android.compose.animation.scene.modifiers.noResizeDuringTransitions import com.android.compose.animation.scene.nestedScrollToScene +import com.android.compose.modifiers.thenIf +import com.android.compose.ui.graphics.ContainerState +import com.android.compose.ui.graphics.container /** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */ @Stable @@ -62,6 +65,7 @@ internal sealed class Content( zIndex: Float, ) { internal val scope = ContentScopeImpl(layoutImpl, content = this) + val containerState = ContainerState() var content by mutableStateOf(content) var zIndex by mutableFloatStateOf(zIndex) @@ -82,6 +86,9 @@ internal sealed class Content( val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.place(0, 0) } } + .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) { + Modifier.container(containerState) + } .testTag(key.testTag) ) { scope.content() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index 9bb302307359..de7f418f219a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -52,6 +52,7 @@ sealed interface Transformation { internal class SharedElementTransformation( override val matcher: ElementMatcher, internal val enabled: Boolean, + internal val elevateInContent: ContentKey?, ) : Transformation /** A transformation that changes the value of an element property, like its size or offset. */ diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt new file mode 100644 index 000000000000..75a5768193cf --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt @@ -0,0 +1,127 @@ +/* + * 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.animation.back + +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.view.animation.Interpolator +import android.window.BackEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.app.animation.Interpolators +import com.android.systemui.SysuiTestCase +import com.android.window.flags.Flags.FLAG_PREDICTIVE_BACK_TIMESTAMP_API +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito + +@SmallTest +@RunWith(AndroidJUnit4::class) +class FlingOnBackAnimationCallbackTest : SysuiTestCase() { + + @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Test + fun testProgressInterpolation() { + val mockInterpolator = Mockito.mock(Interpolator::class.java) + val backEvent = backEventOf(0.5f) + Mockito.`when`(mockInterpolator.getInterpolation(0.5f)).thenReturn(0.8f) + val callback = TestFlingOnBackAnimationCallback(mockInterpolator) + callback.onBackStarted(backEvent) + assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled) + callback.onBackProgressed(backEvent) + assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled) + assertEquals("Assert interpolated progress", 0.8f, callback.progressEvent?.progress) + } + + @Test + @RequiresFlagsEnabled(FLAG_PREDICTIVE_BACK_TIMESTAMP_API) + fun testFling() { + val callback = TestFlingOnBackAnimationCallback(Interpolators.LINEAR) + callback.onBackStarted(backEventOf(progress = 0f, frameTime = 0)) + assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled) + callback.onBackProgressed(backEventOf(0f, 8)) + callback.onBackProgressed(backEventOf(0.2f, 16)) + callback.onBackProgressed(backEventOf(0.4f, 24)) + callback.onBackProgressed(backEventOf(0.6f, 32)) + assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled) + assertEquals("Assert interpolated progress", 0.6f, callback.progressEvent?.progress) + getInstrumentation().runOnMainSync { callback.onBackInvoked() } + // Assert that onBackInvoked is not called immediately... + assertFalse(callback.backInvokedCalled) + // Instead the fling animation is played and eventually onBackInvoked is called. + callback.backInvokedLatch.await(1000, TimeUnit.MILLISECONDS) + assertTrue(callback.backInvokedCalled) + } + + @Test + @RequiresFlagsDisabled(FLAG_PREDICTIVE_BACK_TIMESTAMP_API) + fun testCallbackWithoutTimestampApi() { + // Assert that all callback methods are immediately forwarded + val callback = TestFlingOnBackAnimationCallback(Interpolators.LINEAR) + callback.onBackStarted(backEventOf(progress = 0f, frameTime = 0)) + assertTrue("Assert onBackStartedCompat called", callback.backStartedCalled) + callback.onBackProgressed(backEventOf(0f, 8)) + assertTrue("Assert onBackProgressedCompat called", callback.backProgressedCalled) + callback.onBackInvoked() + assertTrue("Assert onBackInvoked called", callback.backInvokedCalled) + callback.onBackCancelled() + assertTrue("Assert onBackCancelled called", callback.backCancelledCalled) + } + + private fun backEventOf(progress: Float, frameTime: Long = 0): BackEvent { + return BackEvent(10f, 10f, progress, 0, frameTime) + } + + /** Helper class to expose the compat functions for testing */ + private class TestFlingOnBackAnimationCallback(progressInterpolator: Interpolator) : + FlingOnBackAnimationCallback(progressInterpolator) { + var backStartedCalled = false + var backProgressedCalled = false + var backInvokedCalled = false + val backInvokedLatch = CountDownLatch(1) + var backCancelledCalled = false + var progressEvent: BackEvent? = null + + override fun onBackStartedCompat(backEvent: BackEvent) { + backStartedCalled = true + } + + override fun onBackProgressedCompat(backEvent: BackEvent) { + backProgressedCalled = true + progressEvent = backEvent + } + + override fun onBackInvokedCompat() { + backInvokedCalled = true + backInvokedLatch.countDown() + } + + override fun onBackCancelledCompat() { + backCancelledCalled = true + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt index 6e883c24b5d2..9e20e7d0f98c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.hardware.input.fakeInputManager +import android.view.KeyEvent.KEYCODE_1 import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.KEYCODE_B import android.view.KeyEvent.KEYCODE_C @@ -24,6 +25,7 @@ import android.view.KeyEvent.KEYCODE_D import android.view.KeyEvent.KEYCODE_E import android.view.KeyEvent.KEYCODE_F import android.view.KeyEvent.KEYCODE_G +import android.view.KeyEvent.META_FUNCTION_ON import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -87,6 +89,48 @@ class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() { } @Test + fun categories_keycodeAndModifiersAreMappedSeparatelyWhenIdentical() = + testScope.runTest { + fakeSystemSource.setGroups(simpleGroup(simpleShortcutInfo(KEYCODE_1))) + + helper.toggle(deviceId = 123) + val categories by collectLastValue(repo.categories) + + val systemCategory = categories?.firstOrNull { it.type == ShortcutCategoryType.System } + + // Keycode 0x8 should be translated to the Key 1 instead of modifier FN + // which has the same keycode. + val expectedCategory = + ShortcutCategory( + type = ShortcutCategoryType.System, + simpleSubCategory(simpleShortcut("1")), + ) + + assertThat(systemCategory).isEqualTo(expectedCategory) + } + + @Test + fun categories_keyCodeAndModifierHaveSameCode_codesAreMappedCorrectly() = + testScope.runTest { + fakeSystemSource.setGroups(simpleGroup(simpleShortcutInfo(KEYCODE_1, META_FUNCTION_ON))) + + helper.toggle(deviceId = 123) + val categories by collectLastValue(repo.categories) + + val systemCategory = categories?.firstOrNull { it.type == ShortcutCategoryType.System } + + // Keycode 0x8 should be translated to the Key 1 instead of modifier FN + // which has the same keycode. while modifier mask 0x8 should be translated to FN. + val expectedCategory = + ShortcutCategory( + type = ShortcutCategoryType.System, + simpleSubCategory(simpleShortcut("Fn", "1")), + ) + + assertThat(systemCategory).isEqualTo(expectedCategory) + } + + @Test fun categories_multipleSubscribers_replaysExistingValueToNewSubscribers() = testScope.runTest { fakeSystemSource.setGroups(TestShortcuts.systemGroups) @@ -111,24 +155,14 @@ class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() { testScope.runTest { fakeSystemSource.setGroups( listOf( - simpleGroup( - simpleShortcutInfo(KEYCODE_A), - simpleShortcutInfo(KEYCODE_B), - ), - simpleGroup( - simpleShortcutInfo(KEYCODE_C), - ), + simpleGroup(simpleShortcutInfo(KEYCODE_A), simpleShortcutInfo(KEYCODE_B)), + simpleGroup(simpleShortcutInfo(KEYCODE_C)), ) ) fakeMultiTaskingSource.setGroups( listOf( - simpleGroup( - simpleShortcutInfo(KEYCODE_D), - ), - simpleGroup( - simpleShortcutInfo(KEYCODE_E), - simpleShortcutInfo(KEYCODE_F), - ), + simpleGroup(simpleShortcutInfo(KEYCODE_D)), + simpleGroup(simpleShortcutInfo(KEYCODE_E), simpleShortcutInfo(KEYCODE_F)), ) ) fakeAppCategoriesSource.setGroups(listOf(simpleGroup(simpleShortcutInfo(KEYCODE_G)))) @@ -144,16 +178,11 @@ class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() { listOf( simpleSubCategory(simpleShortcut("B")), simpleSubCategory(simpleShortcut("C")), - ) + ), ), ShortcutCategory( ShortcutCategoryType.MultiTasking, - listOf( - simpleSubCategory( - simpleShortcut("E"), - simpleShortcut("F"), - ), - ) + listOf(simpleSubCategory(simpleShortcut("E"), simpleShortcut("F"))), ), ) } @@ -164,14 +193,14 @@ class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() { private fun simpleShortcut(vararg keys: String) = Shortcut( label = simpleShortcutLabel, - commands = listOf(ShortcutCommand(keys.map { ShortcutKey.Text(it) })) + commands = listOf(ShortcutCommand(keys.map { ShortcutKey.Text(it) })), ) private fun simpleGroup(vararg shortcuts: KeyboardShortcutInfo) = KeyboardShortcutGroup(simpleGroupLabel, shortcuts.asList()) - private fun simpleShortcutInfo(keyCode: Int = 0) = - KeyboardShortcutInfo(simpleShortcutLabel, keyCode, /* modifiers= */ 0) + private fun simpleShortcutInfo(keyCode: Int = 0, modifiers: Int = 0) = + KeyboardShortcutInfo(simpleShortcutLabel, keyCode, modifiers) private val simpleShortcutLabel = "shortcut label" private val simpleGroupLabel = "group label" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt index a3f81274dfc5..aceaab8206a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt @@ -60,6 +60,7 @@ class IssueRecordingServiceSessionTest : SysuiTestCase() { private val iActivityManager = mock<IActivityManager>() private val notificationManager = mock<NotificationManager>() private val panelInteractor = mock<PanelInteractor>() + private val screenRecordingStartTimeStore = mock<ScreenRecordingStartTimeStore>() private lateinit var underTest: IssueRecordingServiceSession @@ -76,6 +77,7 @@ class IssueRecordingServiceSessionTest : SysuiTestCase() { iActivityManager, notificationManager, userContextProvider, + screenRecordingStartTimeStore, ) } @@ -90,7 +92,7 @@ class IssueRecordingServiceSessionTest : SysuiTestCase() { @Test fun stopsTracing_afterReceivingStopTracingCommand() { - underTest.stop(mContext.contentResolver) + underTest.stop() bgExecutor.runAllReady() Truth.assertThat(issueRecordingState.isRecording).isFalse() @@ -113,7 +115,7 @@ class IssueRecordingServiceSessionTest : SysuiTestCase() { underTest.share(0, uri) bgExecutor.runAllReady() - verify(iActivityManager).requestBugReportWithExtraAttachment(uri) + verify(iActivityManager).requestBugReportWithExtraAttachments(any()) } @Test @@ -124,7 +126,7 @@ class IssueRecordingServiceSessionTest : SysuiTestCase() { underTest.share(0, uri) bgExecutor.runAllReady() - verify(traceurConnection).shareTraces(uri) + verify(traceurConnection).shareTraces(any()) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStoreTest.kt new file mode 100644 index 000000000000..737b10166199 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStoreTest.kt @@ -0,0 +1,57 @@ +/* + * 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.recordissue + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.settings.UserTracker +import com.android.systemui.settings.userTracker +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class ScreenRecordingStartTimeStoreTest : SysuiTestCase() { + private val userTracker: UserTracker = Kosmos().also { it.testCase = this }.userTracker + + private lateinit var underTest: ScreenRecordingStartTimeStore + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + underTest = ScreenRecordingStartTimeStore(userTracker) + } + + @Test + fun markStartTime_correctlyStoresValues_inSharedPreferences() { + underTest.markStartTime() + + val startTimeMetadata = underTest.userIdToScreenRecordingStartTime.get(userTracker.userId) + Truth.assertThat(startTimeMetadata).isNotNull() + Truth.assertThat(startTimeMetadata!!.getLong(ELAPSED_REAL_TIME_NANOS_KEY)).isNotNull() + Truth.assertThat(startTimeMetadata.getLong(REAL_TO_ELAPSED_TIME_OFFSET_NANOS_KEY)) + .isNotNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index 0d5ddaeedb9e..bff3903e0114 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -52,6 +52,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.recordissue.ScreenRecordingStartTimeStore; import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; @@ -95,6 +96,8 @@ public class RecordingServiceTest extends SysuiTestCase { private SysuiStatusBarStateController mStatusBarStateController; @Mock private ActivityStarter mActivityStarter; + @Mock + private ScreenRecordingStartTimeStore mScreenRecordingStartTimeStore; private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; @@ -108,9 +111,10 @@ public class RecordingServiceTest extends SysuiTestCase { RecordingController controller, Executor executor, Handler handler, UiEventLogger uiEventLogger, NotificationManager notificationManager, - UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) { - super(controller, executor, handler, - uiEventLogger, notificationManager, userContextTracker, keyguardDismissUtil); + UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil, + ScreenRecordingStartTimeStore screenRecordingStartTimeStore) { + super(controller, executor, handler, uiEventLogger, notificationManager, + userContextTracker, keyguardDismissUtil, screenRecordingStartTimeStore); attachBaseContext(mContext); } } @@ -120,7 +124,7 @@ public class RecordingServiceTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mRecordingService = Mockito.spy(new RecordingServiceTestable(mController, mExecutor, mHandler, mUiEventLogger, mNotificationManager, - mUserContextTracker, mKeyguardDismissUtil)); + mUserContextTracker, mKeyguardDismissUtil, mScreenRecordingStartTimeStore)); // Return actual context info doReturn(mContext).when(mRecordingService).getApplicationContext(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt index f38bf13d0bda..ab5fa8ef43fb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QsBatteryModeControllerTest.kt @@ -6,12 +6,11 @@ import android.graphics.Rect import android.view.DisplayCutout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.battery.BatteryMeterView -import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever +import com.android.systemui.res.R +import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule @@ -19,6 +18,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -35,9 +36,12 @@ class QsBatteryModeControllerTest : SysuiTestCase() { const val QS_END_FRAME = 58 } + private val kosmos = testKosmos() + private val insetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore + private val insetsProvider = insetsProviderStore.defaultDisplay + @JvmField @Rule val mockitoRule = MockitoJUnit.rule()!! - @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider @Mock private lateinit var mockedContext: Context @Mock private lateinit var mockedResources: Resources @@ -49,8 +53,7 @@ class QsBatteryModeControllerTest : SysuiTestCase() { whenever(mockedResources.getInteger(R.integer.fade_in_start_frame)).thenReturn(QS_END_FRAME) whenever(mockedResources.getInteger(R.integer.fade_out_complete_frame)) .thenReturn(QQS_START_FRAME) - - controller = QsBatteryModeController(mockedContext, insetsProvider) + controller = QsBatteryModeController(mockedContext, insetsProviderStore) } @Test @@ -96,5 +99,6 @@ class QsBatteryModeControllerTest : SysuiTestCase() { } private fun Int.prevFrameToFraction(): Float = (this - 1) / MOTION_LAYOUT_MAX_FRAME.toFloat() + private fun Int.nextFrameToFraction(): Float = (this + 1) / MOTION_LAYOUT_MAX_FRAME.toFloat() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt new file mode 100644 index 000000000000..0eebab0de831 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt @@ -0,0 +1,77 @@ +/* + * 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.data.repository + +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.unconfinedTestDispatcher +import com.android.systemui.testKosmos +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class MultiDisplayStatusBarContentInsetsProviderStoreTest : SysuiTestCase() { + + private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher } + private val testScope = kosmos.testScope + private val fakeDisplayRepository = kosmos.displayRepository + private val underTest = kosmos.multiDisplayStatusBarContentInsetsProviderStore + + @Before + fun start() { + underTest.start() + } + + @Before fun addDisplays() = runBlocking { fakeDisplayRepository.addDisplay(DEFAULT_DISPLAY) } + + @Test + fun forDisplay_startsInstances() = + testScope.runTest { + val instance = underTest.forDisplay(DEFAULT_DISPLAY) + + verify(instance).start() + } + + @Test + fun beforeDisplayRemoved_doesNotStopInstances() = + testScope.runTest { + val instance = underTest.forDisplay(DEFAULT_DISPLAY) + + verify(instance, never()).stop() + } + + @Test + fun displayRemoved_stopsInstance() = + testScope.runTest { + val instance = underTest.forDisplay(DEFAULT_DISPLAY) + + fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY) + + verify(instance).stop() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index 68e17c1b2d73..157f8189276a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -63,6 +63,7 @@ import com.android.systemui.res.R; import com.android.systemui.shade.ShadeViewStateProvider; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.phone.ui.TintedIconManager; @@ -119,6 +120,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { @Mock private StatusBarContentInsetsProvider mStatusBarContentInsetsProvider; @Mock + private StatusBarContentInsetsProviderStore mStatusBarContentInsetsProviderStore; + @Mock private UserManager mUserManager; @Mock private StatusBarUserChipViewModel mStatusBarUserChipViewModel; @@ -143,7 +146,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mShadeViewStateProvider = new TestShadeViewStateProvider(); MockitoAnnotations.initMocks(this); - + when(mStatusBarContentInsetsProviderStore.getDefaultDisplay()) + .thenReturn(mStatusBarContentInsetsProvider); when(mIconManagerFactory.create(any(), any())).thenReturn(mIconManager); allowTestableLooperAsMainThread(); @@ -175,7 +179,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mKosmos.getKeyguardStatusBarViewModel(), mBiometricUnlockController, mStatusBarStateController, - mStatusBarContentInsetsProvider, + mStatusBarContentInsetsProviderStore, mUserManager, mStatusBarUserChipViewModel, mSecureSettings, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt index ba5fb7f8df00..a862fdfeca22 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt @@ -36,14 +36,15 @@ 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 com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import junit.framework.Assert.assertTrue +import com.google.common.truth.Truth.assertWithMessage import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest @@ -1018,7 +1019,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { // Regression test for b/245799099 @Test - fun onMaxBoundsChanged_listenerNotified() { + fun onMaxBoundsChanged_afterStart_listenerNotified() { // Start out with an existing configuration with bounds configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) configurationController.onConfigurationChanged(configuration) @@ -1038,6 +1039,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { triggered = true } } + provider.start() provider.addCallback(listener) // WHEN the config is updated with new bounds @@ -1049,7 +1051,39 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { } @Test - fun onDensityOrFontScaleChanged_listenerNotified() { + fun onMaxBoundsChanged_beforeStart_listenerNotNotified() { + // Start out with an existing configuration with bounds + configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) + configurationController.onConfigurationChanged(configuration) + val provider = + StatusBarContentInsetsProviderImpl( + contextMock, + configurationController, + mock<DumpManager>(), + mock<CommandRegistry>(), + mock<SysUICutoutProvider>(), + ) + val listener = + object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + // WHEN the config is updated with new bounds + // but provider is not started + configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789) + configurationController.onConfigurationChanged(configuration) + + // THEN the listener is not notified + assertThat(listener.triggered).isFalse() + } + + @Test + fun onDensityOrFontScaleChanged_beforeStart_listenerNotNotified() { configuration.densityDpi = 12 val provider = StatusBarContentInsetsProviderImpl( @@ -1069,6 +1103,36 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { } provider.addCallback(listener) + // WHEN the config is updated, but the provider is not started + configuration.densityDpi = 20 + configurationController.onConfigurationChanged(configuration) + + // THEN the listener is NOT notified + assertThat(listener.triggered).isFalse() + } + + @Test + fun onDensityOrFontScaleChanged_afterStart_listenerNotified() { + configuration.densityDpi = 12 + val provider = + StatusBarContentInsetsProviderImpl( + contextMock, + configurationController, + mock<DumpManager>(), + mock<CommandRegistry>(), + mock<SysUICutoutProvider>(), + ) + val listener = + object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.start() + provider.addCallback(listener) + // WHEN the config is updated configuration.densityDpi = 20 configurationController.onConfigurationChanged(configuration) @@ -1078,7 +1142,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { } @Test - fun onThemeChanged_listenerNotified() { + fun onThemeChanged_afterStart_listenerNotified() { val provider = StatusBarContentInsetsProviderImpl( contextMock, @@ -1095,6 +1159,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { triggered = true } } + provider.start() provider.addCallback(listener) configurationController.notifyThemeChanged() @@ -1103,18 +1168,44 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { assertThat(listener.triggered).isTrue() } + @Test + fun onThemeChanged_beforeStart_listenerNotNotified() { + val provider = + StatusBarContentInsetsProviderImpl( + contextMock, + configurationController, + mock<DumpManager>(), + mock<CommandRegistry>(), + mock<SysUICutoutProvider>(), + ) + val listener = + object : StatusBarContentInsetsChangedListener { + var triggered = false + + override fun onStatusBarContentInsetsChanged() { + triggered = true + } + } + provider.addCallback(listener) + + configurationController.notifyThemeChanged() + + assertThat(listener.triggered).isFalse() + } + private fun assertRects( expected: Rect, actual: Rect, @Rotation currentRotation: Int, @Rotation targetRotation: Int, ) { - assertTrue( - "Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" + - " targetRotation=${RotationUtils.toString(targetRotation)}" + - " expected=$expected actual=$actual", - expected.equals(actual), - ) + assertWithMessage( + "Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" + + " targetRotation=${RotationUtils.toString(targetRotation)}" + + " expected=$expected actual=$actual" + ) + .that(actual) + .isEqualTo(expected) } private fun setNoCutout() { @@ -1136,8 +1227,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { } private fun setCameraProtectionBounds(protectionBounds: Rect) { - val protectionInfo = - mock<CameraProtectionInfo> { whenever(this.bounds).thenReturn(protectionBounds) } + val protectionInfo = mock<CameraProtectionInfo> { on { bounds } doReturn protectionBounds } whenever(sysUICutout.cameraProtection).thenReturn(protectionInfo) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 94753f7e5203..21a317aab36a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -56,6 +57,7 @@ import android.window.WindowOnBackInvokedDispatcher; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; @@ -675,8 +677,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { backCallback.onBackProgressed(event); verify(mBouncerViewDelegateBackCallback).onBackProgressed(eq(event)); - backCallback.onBackInvoked(); - verify(mBouncerViewDelegateBackCallback).onBackInvoked(); + InstrumentationRegistry.getInstrumentation().runOnMainSync(backCallback::onBackInvoked); + verify(mBouncerViewDelegateBackCallback, timeout(1000)).onBackInvoked(); backCallback.onBackCancelled(); verify(mBouncerViewDelegateBackCallback).onBackCancelled(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt index 29e9ba752b36..d9d81692e2b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt @@ -20,6 +20,8 @@ import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection.LEFT +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection.RIGHT 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 @@ -61,23 +63,46 @@ class BackGestureRecognizerTest : SysuiTestCase() { } @Test - fun triggersProgressRelativeToDistance() { - assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f) - assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE / 2, expectedProgress = 0.5f) - assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE, expectedProgress = 1f) - assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE, expectedProgress = 1f) + fun triggersProgressRelativeToDistanceWhenSwipingLeft() { + assertProgressWhileMovingFingers( + deltaX = -SWIPE_DISTANCE / 2, + expected = InProgress(progress = 0.5f, direction = LEFT), + ) + assertProgressWhileMovingFingers( + deltaX = -SWIPE_DISTANCE, + expected = InProgress(progress = 1f, direction = LEFT), + ) } - private fun assertProgressWhileMovingFingers(deltaX: Float, expectedProgress: Float) { + @Test + fun triggersProgressRelativeToDistanceWhenSwipingRight() { + assertProgressWhileMovingFingers( + deltaX = SWIPE_DISTANCE / 2, + expected = InProgress(progress = 0.5f, direction = RIGHT), + ) + assertProgressWhileMovingFingers( + deltaX = SWIPE_DISTANCE, + expected = InProgress(progress = 1f, direction = RIGHT), + ) + } + + private fun assertProgressWhileMovingFingers(deltaX: Float, expected: InProgress) { assertStateAfterEvents( events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) }, - expectedState = InProgress(progress = expectedProgress), + expectedState = expected, ) } @Test fun triggeredProgressIsNoBiggerThanOne() { - assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE * 2, expectedProgress = 1f) + assertProgressWhileMovingFingers( + deltaX = SWIPE_DISTANCE * 2, + expected = InProgress(progress = 1f, direction = RIGHT), + ) + assertProgressWhileMovingFingers( + deltaX = -SWIPE_DISTANCE * 2, + expected = InProgress(progress = 1f, direction = LEFT), + ) } @Test diff --git a/packages/SystemUI/res/layout/contextual_edu_dialog.xml b/packages/SystemUI/res/layout/contextual_edu_dialog.xml index ee42b2363dd5..7eb6efe4afa4 100644 --- a/packages/SystemUI/res/layout/contextual_edu_dialog.xml +++ b/packages/SystemUI/res/layout/contextual_edu_dialog.xml @@ -40,6 +40,5 @@ android:fontFamily="google-sans-medium" android:maxWidth="280dp" android:textColor="?androidprv:attr/materialColorOnTertiaryFixed" - android:textSize="14sp" - android:textStyle="bold" /> + android:textSize="14sp" /> </LinearLayout> diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml index 0029180932ee..2b40df47667a 100644 --- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml +++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml @@ -148,7 +148,7 @@ android:orientation="vertical" android:clickable="false" android:layout_width="wrap_content" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:gravity="start|center_vertical"> <TextView android:id="@+id/mobile_title" diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index bda34530e6eb..1ab9242438b9 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1354,7 +1354,7 @@ <style name="InternetDialog.Network"> <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">88dp</item> + <item name="android:layout_height">wrap_content</item> <item name="android:layout_marginStart">@dimen/internet_dialog_network_layout_margin</item> <item name="android:layout_marginEnd">@dimen/internet_dialog_network_layout_margin</item> <item name="android:layout_gravity">center_vertical|start</item> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 2d27f1c0ca73..6bcacd023c74 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -32,7 +32,6 @@ import static androidx.constraintlayout.widget.ConstraintSet.START; import static androidx.constraintlayout.widget.ConstraintSet.TOP; import static androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT; -import static com.android.app.animation.InterpolatorsAndroidX.DECELERATE_QUINT; import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY; import static java.lang.Integer.max; @@ -271,8 +270,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { public void onBackProgressed(BackEvent event) { float progress = event.getProgress(); // TODO(b/263819310): Update the interpolator to match spec. - float scale = MIN_BACK_SCALE - + (1 - MIN_BACK_SCALE) * (1 - DECELERATE_QUINT.getInterpolation(progress)); + float scale = MIN_BACK_SCALE + (1 - MIN_BACK_SCALE) * (1 - progress); setScale(scale); } }; diff --git a/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt index 00e5405dd904..00e5405dd904 100644 --- a/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt diff --git a/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt index 813e0e025bf5..e6e474aea81d 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner -import com.android.compose.animation.ViewTreeSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.android.systemui.lifecycle.ViewLifecycleOwner /** @@ -88,13 +88,13 @@ object ComposeInitializer { // Set the owners on the root. They will be reused by any ComposeView inside the root // hierarchy. root.setViewTreeLifecycleOwner(lifecycleOwner) - ViewTreeSavedStateRegistryOwner.set(root, savedStateRegistryOwner) + root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner) } /** Function to be called on your window root view's [View.onDetachedFromWindow] function. */ fun onDetachedFromWindow(root: View) { (root.findViewTreeLifecycleOwner() as ViewLifecycleOwner).onDestroy() root.setViewTreeLifecycleOwner(null) - ViewTreeSavedStateRegistryOwner.set(root, null) + root.setViewTreeSavedStateRegistryOwner(null) } } diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt index 2ce3e43389fa..2c4345556f15 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt @@ -76,7 +76,7 @@ abstract class PerDisplayStoreImpl<T>( } } - abstract fun createInstanceForDisplay(displayId: Int): T + protected abstract fun createInstanceForDisplay(displayId: Int): T override fun start() { val instanceType = instanceClass.simpleName diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt index 4142be3f9358..058e5874ae7c 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionKeyTutorialScreen.kt @@ -52,7 +52,7 @@ fun ActionKeyTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) Modifier.fillMaxSize() .onKeyEvent { keyEvent: KeyEvent -> if (keyEvent.key == Key.MetaLeft && keyEvent.type == KeyEventType.KeyUp) { - actionState = Finished + actionState = Finished(R.raw.action_key_success) } true } @@ -80,11 +80,7 @@ private fun buildScreenConfig() = titleSuccessResId = R.string.tutorial_action_key_success_title, bodySuccessResId = R.string.tutorial_action_key_success_body, ), - animations = - TutorialScreenConfig.Animations( - educationResId = R.raw.action_key_edu, - successResId = R.raw.action_key_success, - ), + animations = TutorialScreenConfig.Animations(educationResId = R.raw.action_key_edu), ) @Composable diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt index 8e01e3765c32..3d2baee9b936 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt @@ -16,6 +16,7 @@ package com.android.systemui.inputdevice.tutorial.ui.composable +import androidx.annotation.RawRes import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -48,7 +49,7 @@ sealed interface TutorialActionState { val endMarker: String? = null, ) : TutorialActionState - data object Finished : TutorialActionState + data class Finished(@RawRes val successAnimation: Int) : TutorialActionState } @Composable @@ -68,11 +69,11 @@ fun ActionTutorialContent( Row(modifier = Modifier.fillMaxWidth().weight(1f)) { TutorialDescription( titleTextId = - if (actionState == Finished) config.strings.titleSuccessResId + if (actionState is Finished) config.strings.titleSuccessResId else config.strings.titleResId, titleColor = config.colors.title, bodyTextId = - if (actionState == Finished) config.strings.bodySuccessResId + if (actionState is Finished) config.strings.bodySuccessResId else config.strings.bodyResId, modifier = Modifier.weight(1f), ) @@ -83,7 +84,7 @@ fun ActionTutorialContent( modifier = Modifier.weight(1f).padding(top = 8.dp), ) } - AnimatedVisibility(visible = actionState == Finished, enter = fadeIn()) { + AnimatedVisibility(visible = actionState is Finished, enter = fadeIn()) { DoneButton(onDoneButtonClicked = onDoneButtonClicked) } } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt index ef375a868cef..720c01fc7056 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialAnimation.kt @@ -77,7 +77,9 @@ fun TutorialAnimation( config.colors.animationColors, ) Finished::class -> - SuccessAnimation(config.animations.successResId, config.colors.animationColors) + // Below cast is safe as Finished state is the last state and afterwards we can + // only leave the screen so this composable would be no longer displayed + SuccessAnimation(actionState as Finished, config.colors.animationColors) } } } @@ -100,10 +102,11 @@ private fun EducationAnimation( @Composable private fun SuccessAnimation( - @RawRes successAnimationId: Int, + finishedState: Finished, animationProperties: LottieDynamicProperties, ) { - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId)) + val composition by + rememberLottieComposition(LottieCompositionSpec.RawRes(finishedState.successAnimation)) val progress by animateLottieCompositionAsState(composition, iterations = 1) LottieAnimation( composition = composition, diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt index 55e5f2d79e60..60dfed3a67a4 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialScreenConfig.kt @@ -24,13 +24,13 @@ import com.airbnb.lottie.compose.LottieDynamicProperties data class TutorialScreenConfig( val colors: Colors, val strings: Strings, - val animations: Animations + val animations: Animations, ) { data class Colors( val background: Color, val title: Color, - val animationColors: LottieDynamicProperties + val animationColors: LottieDynamicProperties, ) data class Strings( @@ -40,8 +40,5 @@ data class TutorialScreenConfig( @StringRes val bodySuccessResId: Int, ) - data class Animations( - @RawRes val educationResId: Int, - @RawRes val successResId: Int, - ) + data class Animations(@RawRes val educationResId: Int) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt index 85bd0b0b1968..a08588750f85 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt @@ -68,7 +68,7 @@ constructor( @InputShortcuts private val inputShortcutsSource: KeyboardShortcutGroupsSource, @CurrentAppShortcuts private val currentAppShortcutsSource: KeyboardShortcutGroupsSource, private val inputManager: InputManager, - stateRepository: ShortcutHelperStateRepository + stateRepository: ShortcutHelperStateRepository, ) { private val sources = @@ -76,27 +76,27 @@ constructor( InternalGroupsSource( source = systemShortcutsSource, isTrusted = true, - typeProvider = { System } + typeProvider = { System }, ), InternalGroupsSource( source = multitaskingShortcutsSource, isTrusted = true, - typeProvider = { MultiTasking } + typeProvider = { MultiTasking }, ), InternalGroupsSource( source = appCategoriesShortcutsSource, isTrusted = true, - typeProvider = { AppCategories } + typeProvider = { AppCategories }, ), InternalGroupsSource( source = inputShortcutsSource, isTrusted = false, - typeProvider = { InputMethodEditor } + typeProvider = { InputMethodEditor }, ), InternalGroupsSource( source = currentAppShortcutsSource, isTrusted = false, - typeProvider = { groups -> getCurrentAppShortcutCategoryType(groups) } + typeProvider = { groups -> getCurrentAppShortcutCategoryType(groups) }, ), ) @@ -179,7 +179,7 @@ constructor( shortcutGroup.items, keepIcons, supportedKeyCodes, - ) + ), ) } .filter { it.shortcuts.isNotEmpty() } @@ -214,13 +214,13 @@ constructor( return Shortcut( label = shortcutInfo.label!!.toString(), icon = toShortcutIcon(keepIcon, shortcutInfo), - commands = listOf(shortcutCommand) + commands = listOf(shortcutCommand), ) } private fun toShortcutIcon( keepIcon: Boolean, - shortcutInfo: KeyboardShortcutInfo + shortcutInfo: KeyboardShortcutInfo, ): ShortcutIcon? { if (!keepIcon) { return null @@ -236,13 +236,13 @@ constructor( private fun toShortcutCommand( keyCharacterMap: KeyCharacterMap, - info: KeyboardShortcutInfo + info: KeyboardShortcutInfo, ): ShortcutCommand? { val keys = mutableListOf<ShortcutKey>() var remainingModifiers = info.modifiers SUPPORTED_MODIFIERS.forEach { supportedModifier -> if ((supportedModifier and remainingModifiers) != 0) { - keys += toShortcutKey(keyCharacterMap, supportedModifier) ?: return null + keys += toShortcutModifierKey(supportedModifier) ?: return null // "Remove" the modifier from the remaining modifiers remainingModifiers = remainingModifiers and supportedModifier.inv() } @@ -262,6 +262,20 @@ constructor( return ShortcutCommand(keys) } + private fun toShortcutModifierKey(modifierMask: Int): ShortcutKey? { + val iconResId = ShortcutHelperKeys.keyIcons[modifierMask] + if (iconResId != null) { + return ShortcutKey.Icon(iconResId) + } + + val modifierLabel = ShortcutHelperKeys.modifierLabels[modifierMask] + if (modifierLabel != null) { + return ShortcutKey.Text(modifierLabel(context)) + } + Log.wtf("TAG", "Couldn't find label or icon for modifier $modifierMask") + return null + } + private fun toShortcutKey( keyCharacterMap: KeyCharacterMap, keyCode: Int, @@ -289,7 +303,7 @@ constructor( private suspend fun fetchSupportedKeyCodes( deviceId: Int, - groupsFromAllSources: List<List<KeyboardShortcutGroup>> + groupsFromAllSources: List<List<KeyboardShortcutGroup>>, ): Set<Int> = withContext(backgroundDispatcher) { val allUsedKeyCodes = @@ -320,7 +334,7 @@ constructor( KeyEvent.META_ALT_ON, KeyEvent.META_SHIFT_ON, KeyEvent.META_SYM_ON, - KeyEvent.META_FUNCTION_ON + KeyEvent.META_FUNCTION_ON, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt index 8db16fa9a06e..288efa275219 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt @@ -124,6 +124,17 @@ object ShortcutHelperKeys { KEYCODE_RECENT_APPS to R.drawable.ic_check_box_outline_blank, ) + val modifierLabels = + mapOf<Int, (Context) -> String>( + // Modifiers + META_META_ON to { "Meta" }, + META_CTRL_ON to { "Ctrl" }, + META_ALT_ON to { "Alt" }, + META_SHIFT_ON to { "Shift" }, + META_SYM_ON to { "Sym" }, + META_FUNCTION_ON to { "Fn" }, + ) + val specialKeyLabels = mapOf<Int, (Context) -> String>( KEYCODE_HOME to { context -> context.getString(R.string.keyboard_key_home) }, @@ -317,7 +328,7 @@ object ShortcutHelperKeys { { context -> context.getString( R.string.keyboard_key_numpad_template, - context.getString(R.string.keyboard_key_enter) + context.getString(R.string.keyboard_key_enter), ) }, KEYCODE_NUMPAD_EQUALS to @@ -343,13 +354,5 @@ object ShortcutHelperKeys { KEYCODE_CTRL_RIGHT to { "Ctrl" }, KEYCODE_SHIFT_LEFT to { "Shift" }, KEYCODE_SHIFT_RIGHT to { "Shift" }, - - // Modifiers - META_META_ON to { "Meta" }, - META_CTRL_ON to { "Ctrl" }, - META_ALT_ON to { "Alt" }, - META_SHIFT_ON to { "Shift" }, - META_SYM_ON to { "Sym" }, - META_FUNCTION_ON to { "Fn" }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt new file mode 100644 index 000000000000..ffeec4e0480c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.flags + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.shared.flag.DualShade + +/** Helper for reading or using the QS Detailed View flag state. */ +@Suppress("NOTHING_TO_INLINE") +object QsDetailedView { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_QS_TILE_DETAILED_VIEW + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the flag enabled */ + @JvmStatic + inline val isEnabled + get() = + Flags.qsTileDetailedView() && // mainAconfigFlag + DualShade.isEnabled && + SceneContainerFlag.isEnabled + + // NOTE: Changes should also be made in getSecondaryFlags + + /** The main aconfig flag. */ + inline fun getMainAconfigFlag() = FlagToken(FLAG_NAME, Flags.qsTileDetailedView()) + + /** The set of secondary flags which must be enabled for qs detailed view to work properly */ + inline fun getSecondaryFlags(): Sequence<FlagToken> = + sequenceOf( + DualShade.token + // NOTE: Changes should also be made in isEnabled + ) + SceneContainerFlag.getAllRequirements() + + /** The full set of requirements for QsDetailedView */ + inline fun getAllRequirements(): Sequence<FlagToken> { + return sequenceOf(getMainAconfigFlag()) + getSecondaryFlags() + } + + /** Return all dependencies of this flag in pairs where [Pair.first] depends on [Pair.second] */ + inline fun getFlagDependencies(): Sequence<Pair<FlagToken, FlagToken>> { + val mainAconfigFlag = getMainAconfigFlag() + return getSecondaryFlags().map { mainAconfigFlag to it } + } + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) + + /** Returns a developer-readable string that describes the current requirement list. */ + @JvmStatic + fun requirementDescription(): String { + return buildString { + getAllRequirements().forEach { requirement -> + append('\n') + append(if (requirement.isEnabled) " [MET]" else "[NOT MET]") + append(" ${requirement.name}") + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index ad0ede44a58e..32d9ba822382 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -24,11 +24,9 @@ import android.content.res.Resources import android.net.Uri import android.os.Handler import android.os.IBinder -import android.os.Looper import android.util.Log import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.DialogTransitionAnimator -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.LongRunning import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor @@ -39,6 +37,7 @@ import com.android.systemui.screenrecord.RecordingServiceStrings import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.traceur.MessageConstants.INTENT_EXTRA_TRACE_TYPE +import com.android.traceur.PresetTraceConfigs import com.android.traceur.TraceConfig import java.util.concurrent.Executor import javax.inject.Inject @@ -47,7 +46,6 @@ class IssueRecordingService @Inject constructor( controller: RecordingController, - @Background private val bgLooper: Looper, @LongRunning private val bgExecutor: Executor, @Main handler: Handler, uiEventLogger: UiEventLogger, @@ -59,6 +57,7 @@ constructor( private val issueRecordingState: IssueRecordingState, traceurConnectionProvider: TraceurConnection.Provider, iActivityManager: IActivityManager, + screenRecordingStartTimeStore: ScreenRecordingStartTimeStore, ) : RecordingService( controller, @@ -68,6 +67,7 @@ constructor( notificationManager, userContextProvider, keyguardDismissUtil, + screenRecordingStartTimeStore, ) { private val traceurConnection: TraceurConnection = traceurConnectionProvider.create() @@ -82,6 +82,7 @@ constructor( iActivityManager, notificationManager, userContextProvider, + screenRecordingStartTimeStore, ) /** @@ -111,21 +112,23 @@ constructor( Log.d(getTag(), "handling action: ${intent?.action}") when (intent?.action) { ACTION_START -> { - session.start() + val screenRecord = intent.getBooleanExtra(EXTRA_SCREEN_RECORD, false) with(session) { traceConfig = intent.getParcelableExtra(INTENT_EXTRA_TRACE_TYPE, TraceConfig::class.java) + ?: PresetTraceConfigs.getDefaultConfig() takeBugReport = intent.getBooleanExtra(EXTRA_BUG_REPORT, false) + this.screenRecord = screenRecord start() } - if (!intent.getBooleanExtra(EXTRA_SCREEN_RECORD, false)) { + if (!screenRecord) { // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action // will circumvent the RecordingService's screen recording start code. return super.onStartCommand(Intent(ACTION_SHOW_START_NOTIF), flags, startId) } } ACTION_STOP, - ACTION_STOP_NOTIF -> session.stop(contentResolver) + ACTION_STOP_NOTIF -> session.stop() ACTION_SHARE -> { session.share( intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId), diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt index 4c01293d1cad..43539335d8e4 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceSession.kt @@ -18,7 +18,7 @@ package com.android.systemui.recordissue import android.app.IActivityManager import android.app.NotificationManager -import android.content.ContentResolver +import android.content.Intent import android.net.Uri import android.os.UserHandle import android.provider.Settings @@ -28,6 +28,7 @@ import com.android.systemui.settings.UserContextProvider import com.android.traceur.PresetTraceConfigs import java.util.concurrent.Executor +private const val SHELL_PACKAGE = "com.android.shell" private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended" private const val DISABLED = 0 @@ -47,19 +48,25 @@ class IssueRecordingServiceSession( private val iActivityManager: IActivityManager, private val notificationManager: NotificationManager, private val userContextProvider: UserContextProvider, + private val startTimeStore: ScreenRecordingStartTimeStore, ) { var takeBugReport = false var traceConfig = PresetTraceConfigs.getDefaultConfig() + var screenRecord = false fun start() { bgExecutor.execute { traceurConnection.startTracing(traceConfig) } issueRecordingState.isRecording = true } - fun stop(contentResolver: ContentResolver) { + fun stop() { bgExecutor.execute { if (traceConfig.longTrace) { - Settings.Global.putInt(contentResolver, NOTIFY_SESSION_ENDED_SETTING, DISABLED) + Settings.Global.putInt( + userContextProvider.userContext.contentResolver, + NOTIFY_SESSION_ENDED_SETTING, + DISABLED, + ) } traceurConnection.stopTracing() } @@ -73,11 +80,24 @@ class IssueRecordingServiceSession( notificationId, UserHandle(userContextProvider.userContext.userId), ) - + val screenRecordingUris: List<Uri> = + mutableListOf<Uri>().apply { + screenRecording?.let { add(it) } + if (traceConfig.winscope && screenRecord) { + startTimeStore.getFileUri(userContextProvider.userContext)?.let { add(it) } + } + } if (takeBugReport) { - iActivityManager.requestBugReportWithExtraAttachment(screenRecording) + screenRecordingUris.forEach { + userContextProvider.userContext.grantUriPermission( + SHELL_PACKAGE, + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ) + } + iActivityManager.requestBugReportWithExtraAttachments(screenRecordingUris) } else { - traceurConnection.shareTraces(screenRecording) + traceurConnection.shareTraces(screenRecordingUris) } } diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStore.kt b/packages/SystemUI/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStore.kt new file mode 100644 index 000000000000..5d8bc5564f10 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recordissue/ScreenRecordingStartTimeStore.kt @@ -0,0 +1,78 @@ +/* + * 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.recordissue + +import android.content.Context +import android.net.Uri +import android.os.SystemClock +import android.util.Log +import android.util.SparseArray +import androidx.annotation.VisibleForTesting +import androidx.core.content.FileProvider +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.settings.UserTracker +import java.io.File +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import org.json.JSONObject + +private const val TAG = "ScreenRecordingStartTimeStore" +@VisibleForTesting const val REAL_TO_ELAPSED_TIME_OFFSET_NANOS_KEY = "realToElapsedTimeOffsetNanos" +@VisibleForTesting const val ELAPSED_REAL_TIME_NANOS_KEY = "elapsedRealTimeNanos" +private const val RECORDING_METADATA_FILE_SUFFIX = "screen_recording_metadata.json" +private const val AUTHORITY = "com.android.systemui.fileprovider" + +@SysUISingleton +class ScreenRecordingStartTimeStore @Inject constructor(private val userTracker: UserTracker) { + @VisibleForTesting val userIdToScreenRecordingStartTime = SparseArray<JSONObject>() + + fun markStartTime() { + val elapsedRealTimeNano = SystemClock.elapsedRealtimeNanos() + val realToElapsedTimeOffsetNano = + TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) - + SystemClock.elapsedRealtimeNanos() + val startTimeMetadata = + JSONObject() + .put(ELAPSED_REAL_TIME_NANOS_KEY, elapsedRealTimeNano) + .put(REAL_TO_ELAPSED_TIME_OFFSET_NANOS_KEY, realToElapsedTimeOffsetNano) + userIdToScreenRecordingStartTime.put(userTracker.userId, startTimeMetadata) + } + + /** + * Outputs start time metadata as Json to a file that can then be shared. Returns the Uri or + * null if the file system is not usable and the start time meta data is available. Uses + * com.android.systemui.fileprovider's authority. + * + * Because this file is not uniquely named, it doesn't need to be cleaned up. Every time it is + * outputted, it will overwrite the last file's contents. This is a feature, not a bug. + */ + fun getFileUri(context: Context): Uri? { + val dir = context.externalCacheDir?.apply { mkdirs() } ?: return null + try { + val outFile = + File(dir, RECORDING_METADATA_FILE_SUFFIX).apply { + userIdToScreenRecordingStartTime.get(userTracker.userId)?.let { + writeText(it.toString()) + } ?: return null + } + return FileProvider.getUriForFile(context, AUTHORITY, outFile) + } catch (e: Exception) { + Log.e(TAG, "failed to get screen recording start time metadata via file uri", e) + return null + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurConnection.kt b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurConnection.kt index 81529b357527..e6df3cd59ec9 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/TraceurConnection.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/TraceurConnection.kt @@ -76,8 +76,9 @@ private constructor(userContextProvider: UserContextProvider, private val bgLoop @WorkerThread fun stopTracing() = sendMessage(MessageConstants.STOP_WHAT) @WorkerThread - fun shareTraces(screenRecord: Uri?) { - val replyHandler = Messenger(ShareFilesHandler(screenRecord, userContextProvider, bgLooper)) + fun shareTraces(screenRecordingUris: List<Uri>) { + val replyHandler = + Messenger(ShareFilesHandler(screenRecordingUris, userContextProvider, bgLooper)) sendMessage(MessageConstants.SHARE_WHAT, replyTo = replyHandler) } @@ -101,7 +102,7 @@ private constructor(userContextProvider: UserContextProvider, private val bgLoop } private class ShareFilesHandler( - private val screenRecord: Uri?, + private val screenRecordingUris: List<Uri>, private val userContextProvider: UserContextProvider, looper: Looper, ) : Handler(looper) { @@ -122,7 +123,7 @@ private class ShareFilesHandler( ArrayList<Uri>().apply { perfetto?.let { add(it) } winscope?.let { add(it) } - screenRecord?.let { add(it) } + screenRecordingUris.forEach { add(it) } } val fileSharingIntent = FileSender.buildSendIntent(userContextProvider.userContext, uris) diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index c3de06778ee5..5028c2ea8f0a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -43,6 +43,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget; +import com.android.systemui.recordissue.ScreenRecordingStartTimeStore; import com.android.systemui.res.R; import com.android.systemui.screenrecord.ScreenMediaRecorder.ScreenMediaRecorderListener; import com.android.systemui.settings.UserContextProvider; @@ -92,6 +93,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList private boolean mShowTaps; private boolean mOriginalShowTaps; private ScreenMediaRecorder mRecorder; + private final ScreenRecordingStartTimeStore mScreenRecordingStartTimeStore; private final Executor mLongExecutor; private final UiEventLogger mUiEventLogger; protected final NotificationManager mNotificationManager; @@ -103,7 +105,8 @@ public class RecordingService extends Service implements ScreenMediaRecorderList public RecordingService(RecordingController controller, @LongRunning Executor executor, @Main Handler handler, UiEventLogger uiEventLogger, NotificationManager notificationManager, - UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) { + UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil, + ScreenRecordingStartTimeStore screenRecordingStartTimeStore) { mController = controller; mLongExecutor = executor; mMainHandler = handler; @@ -111,6 +114,7 @@ public class RecordingService extends Service implements ScreenMediaRecorderList mNotificationManager = notificationManager; mUserContextTracker = userContextTracker; mKeyguardDismissUtil = keyguardDismissUtil; + mScreenRecordingStartTimeStore = screenRecordingStartTimeStore; } /** @@ -178,7 +182,8 @@ public class RecordingService extends Service implements ScreenMediaRecorderList currentUid, mAudioSource, captureTarget, - this + this, + mScreenRecordingStartTimeStore ); if (startRecording()) { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java index e024710ed3eb..54da1b04aeb4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java @@ -55,6 +55,7 @@ import android.view.WindowManager; import com.android.internal.R; import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget; +import com.android.systemui.recordissue.ScreenRecordingStartTimeStore; import java.io.Closeable; import java.io.File; @@ -91,6 +92,7 @@ public class ScreenMediaRecorder extends MediaProjection.Callback { private ScreenInternalAudioRecorder mAudio; private ScreenRecordingAudioSource mAudioSource; private final MediaProjectionCaptureTarget mCaptureRegion; + private final ScreenRecordingStartTimeStore mScreenRecordingStartTimeStore; private final Handler mHandler; private Context mContext; @@ -99,13 +101,15 @@ public class ScreenMediaRecorder extends MediaProjection.Callback { public ScreenMediaRecorder(Context context, Handler handler, int uid, ScreenRecordingAudioSource audioSource, MediaProjectionCaptureTarget captureRegion, - ScreenMediaRecorderListener listener) { + ScreenMediaRecorderListener listener, + ScreenRecordingStartTimeStore screenRecordingStartTimeStore) { mContext = context; mHandler = handler; mUid = uid; mCaptureRegion = captureRegion; mListener = listener; mAudioSource = audioSource; + mScreenRecordingStartTimeStore = screenRecordingStartTimeStore; } private void prepare() throws IOException, RemoteException, RuntimeException { @@ -278,6 +282,7 @@ public class ScreenMediaRecorder extends MediaProjection.Callback { Log.d(TAG, "start recording"); prepare(); mMediaRecorder.start(); + mScreenRecordingStartTimeStore.markStartTime(); recordInternalAudio(); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt b/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt index 1fcb70c6bc95..91627d63980e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/QsBatteryModeController.kt @@ -2,9 +2,9 @@ package com.android.systemui.shade import android.content.Context import android.view.DisplayCutout -import com.android.systemui.res.R import com.android.systemui.battery.BatteryMeterView -import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider +import com.android.systemui.res.R +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import javax.inject.Inject /** @@ -15,9 +15,11 @@ class QsBatteryModeController @Inject constructor( private val context: Context, - private val insetsProvider: StatusBarContentInsetsProvider, + insetsProviderStore: StatusBarContentInsetsProviderStore, ) { + private val insetsProvider = insetsProviderStore.defaultDisplay + private companion object { // MotionLayout frames are in [0, 100]. Where 0 and 100 are reserved for start and end // frames. @@ -65,6 +67,5 @@ constructor( private fun hasCenterCutout(cutout: DisplayCutout?): Boolean = cutout?.let { !insetsProvider.currentRotationHasCornerCutout() && !it.boundingRectTop.isEmpty - } - ?: false + } ?: false } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt new file mode 100644 index 000000000000..c72db56a822f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -0,0 +1,71 @@ +/* + * 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 android.content.Context +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY +import com.android.systemui.Flags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.res.R +import dagger.Module +import dagger.Provides + +/** + * Module responsible for managing display-specific components and resources for the notification + * shade window. + * + * This isolation is crucial because when the window transitions between displays, its associated + * context, resources, and display characteristics (like density and size) also change. If the shade + * window shared the same context as the rest of the system UI, it could lead to inconsistencies and + * errors due to incorrect display information. + * + * By using this dedicated module, we ensure the notification shade window always utilizes the + * correct display context and resources, regardless of the display it's on. + */ +@Module +object ShadeDisplayAwareModule { + + /** Creates a new context for the shade window. */ + @Provides + @ShadeDisplayAware + @SysUISingleton + fun provideShadeDisplayAwareContext(context: Context): Context { + return if (Flags.shadeWindowGoesAround()) { + context + .createWindowContext(context.display, TYPE_APPLICATION_OVERLAY, /* options= */ null) + .apply { setTheme(R.style.Theme_SystemUI) } + } else { + context + } + } + + @Provides + @ShadeDisplayAware + @SysUISingleton + fun provideShadeDisplayAwareResources(@ShadeDisplayAware context: Context): Resources { + return context.resources + } + + @Provides + @ShadeDisplayAware + @SysUISingleton + fun providesDisplayAwareLayoutInflater(@ShadeDisplayAware context: Context): LayoutInflater { + return LayoutInflater.from(context) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt index cb589aa10cd9..d0f038698a8e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt @@ -57,7 +57,7 @@ import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONS import com.android.systemui.shade.ShadeViewProviderModule.Companion.SHADE_HEADER import com.android.systemui.shade.carrier.ShadeCarrierGroup import com.android.systemui.shade.carrier.ShadeCarrierGroupController -import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory @@ -90,7 +90,7 @@ constructor( private val statusBarIconController: StatusBarIconController, private val tintedIconManagerFactory: TintedIconManager.Factory, private val privacyIconsController: HeaderPrivacyIconsController, - private val insetsProvider: StatusBarContentInsetsProvider, + private val insetsProviderStore: StatusBarContentInsetsProviderStore, private val configurationController: ConfigurationController, private val variableDateViewControllerFactory: VariableDateViewController.Factory, @Named(SHADE_HEADER) private val batteryMeterViewController: BatteryMeterViewController, @@ -104,6 +104,8 @@ constructor( private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory, ) : ViewController<View>(header), Dumpable { + private val insetsProvider = insetsProviderStore.defaultDisplay + companion object { /** IDs for transitions and constraints for the [MotionLayout]. */ @VisibleForTesting internal val HEADER_TRANSITION_ID = R.id.header_transition @@ -262,7 +264,7 @@ constructor( left, header.paddingTop, header.paddingRight, - header.paddingBottom + header.paddingBottom, ) systemIconsHoverContainer.setPaddingRelative( resources.getDimensionPixelSize( @@ -276,7 +278,7 @@ constructor( ), resources.getDimensionPixelSize( R.dimen.hover_system_icons_container_padding_bottom - ) + ), ) } @@ -317,7 +319,7 @@ constructor( batteryIcon.updateColors( fgColor /* foreground */, bgColor /* background */, - fgColor /* single tone (current default) */ + fgColor, /* single tone (current default) */ ) carrierIconSlots = @@ -426,7 +428,7 @@ constructor( if (view.isLayoutRtl) cutoutRight else cutoutLeft, header.paddingStart, if (view.isLayoutRtl) cutoutLeft else cutoutRight, - header.paddingEnd + header.paddingEnd, ) if (cutout != null) { @@ -437,7 +439,7 @@ constructor( changes += combinedShadeHeadersConstraintManager.centerCutoutConstraints( view.isLayoutRtl, - (view.width - view.paddingLeft - view.paddingRight - topCutout.width()) / 2 + (view.width - view.paddingLeft - view.paddingRight - topCutout.width()) / 2, ) } } else { @@ -563,7 +565,7 @@ constructor( clockPaddingStart, clock.paddingTop, clockPaddingEnd, - clock.paddingBottom + clock.paddingBottom, ) } @@ -602,9 +604,8 @@ constructor( @VisibleForTesting internal fun simulateViewDetached() = this.onViewDetached() - inner class CustomizerAnimationListener( - private val enteringCustomizing: Boolean, - ) : AnimatorListenerAdapter() { + inner class CustomizerAnimationListener(private val enteringCustomizing: Boolean) : + AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) header.animate().setListener(null) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 2348a110eb3a..6f5547a62472 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -49,7 +49,10 @@ import dagger.Provides import javax.inject.Provider /** Module for classes related to the notification shade. */ -@Module(includes = [StartShadeModule::class, ShadeViewProviderModule::class]) +@Module( + includes = + [StartShadeModule::class, ShadeViewProviderModule::class, ShadeDisplayAwareModule::class] +) abstract class ShadeModule { companion object { @Provides diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt index e6270b8740a6..c416bf7b4f92 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.data import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepositoryModule import com.android.systemui.statusbar.data.repository.RemoteInputRepositoryModule import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerModule +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStoreModule import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryModule import com.android.systemui.statusbar.phone.data.StatusBarPhoneDataLayerModule import dagger.Module @@ -28,6 +29,7 @@ import dagger.Module KeyguardStatusBarRepositoryModule::class, RemoteInputRepositoryModule::class, StatusBarConfigurationControllerModule::class, + StatusBarContentInsetsProviderStoreModule::class, StatusBarModeRepositoryModule::class, StatusBarPhoneDataLayerModule::class, ] diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt new file mode 100644 index 000000000000..e471b12c1d58 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt @@ -0,0 +1,119 @@ +/* + * 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.data.repository + +import android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR +import com.android.systemui.CameraProtectionLoaderImpl +import com.android.systemui.CoreStartable +import com.android.systemui.SysUICutoutProviderImpl +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository +import com.android.systemui.display.data.repository.PerDisplayStore +import com.android.systemui.display.data.repository.PerDisplayStoreImpl +import com.android.systemui.display.data.repository.SingleDisplayStore +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl +import dagger.Lazy +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** Provides per display instances of [StatusBarContentInsetsProvider]. */ +interface StatusBarContentInsetsProviderStore : PerDisplayStore<StatusBarContentInsetsProvider> + +@SysUISingleton +class MultiDisplayStatusBarContentInsetsProviderStore +@Inject +constructor( + @Background backgroundApplicationScope: CoroutineScope, + displayRepository: DisplayRepository, + private val factory: StatusBarContentInsetsProviderImpl.Factory, + private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository, + private val statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore, + private val sysUICutoutProviderFactory: SysUICutoutProviderImpl.Factory, + private val cameraProtectionLoaderFactory: CameraProtectionLoaderImpl.Factory, +) : + StatusBarContentInsetsProviderStore, + PerDisplayStoreImpl<StatusBarContentInsetsProvider>( + backgroundApplicationScope, + displayRepository, + ) { + + override fun createInstanceForDisplay(displayId: Int): StatusBarContentInsetsProvider { + val context = displayWindowPropertiesRepository.get(displayId, TYPE_STATUS_BAR).context + val cameraProtectionLoader = cameraProtectionLoaderFactory.create(context) + return factory + .create( + context, + statusBarConfigurationControllerStore.forDisplay(displayId), + sysUICutoutProviderFactory.create(context, cameraProtectionLoader), + ) + .also { it.start() } + } + + override suspend fun onDisplayRemovalAction(instance: StatusBarContentInsetsProvider) { + instance.stop() + } + + override val instanceClass = StatusBarContentInsetsProvider::class.java +} + +@SysUISingleton +class SingleDisplayStatusBarContentInsetsProviderStore +@Inject +constructor(statusBarContentInsetsProvider: StatusBarContentInsetsProvider) : + StatusBarContentInsetsProviderStore, + PerDisplayStore<StatusBarContentInsetsProvider> by SingleDisplayStore( + defaultInstance = statusBarContentInsetsProvider + ) + +@Module +object StatusBarContentInsetsProviderStoreModule { + + @Provides + @SysUISingleton + @IntoMap + @ClassKey(StatusBarContentInsetsProviderStore::class) + fun storeAsCoreStartable( + multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsProviderStore> + ): CoreStartable { + return if (StatusBarConnectedDisplays.isEnabled) { + return multiDisplayLazy.get() + } else { + CoreStartable.NOP + } + } + + @Provides + @SysUISingleton + fun store( + singleDisplayLazy: Lazy<SingleDisplayStatusBarContentInsetsProviderStore>, + multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsProviderStore>, + ): StatusBarContentInsetsProviderStore { + return if (StatusBarConnectedDisplays.isEnabled) { + multiDisplayLazy.get() + } else { + singleDisplayLazy.get() + } + } +} 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 0eef8d63c2d1..2506c95ad800 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -34,6 +34,7 @@ import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.policy.ConfigurationController @@ -730,8 +731,12 @@ object PrivacyDotViewControllerModule { factory: PrivacyDotViewControllerImpl.Factory, @Application scope: CoroutineScope, configurationController: ConfigurationController, - contentInsetsProvider: StatusBarContentInsetsProvider, + contentInsetsProviderStore: StatusBarContentInsetsProviderStore, ): PrivacyDotViewController { - return factory.create(scope, configurationController, contentInsetsProvider) + return factory.create( + scope, + configurationController, + contentInsetsProviderStore.defaultDisplay, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt index 35816c25e976..b28660590ad0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt @@ -33,6 +33,7 @@ import androidx.core.animation.ValueAnimator import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.window.StatusBarWindowController @@ -453,12 +454,12 @@ object SystemEventChipAnimationControllerModule { factory: SystemEventChipAnimationControllerImpl.Factory, context: Context, statusBarWindowControllerStore: StatusBarWindowControllerStore, - contentInsetsProvider: StatusBarContentInsetsProvider, + contentInsetsProviderStore: StatusBarContentInsetsProviderStore, ): SystemEventChipAnimationController { return factory.create( context, statusBarWindowControllerStore.defaultDisplay, - contentInsetsProvider, + contentInsetsProviderStore.defaultDisplay, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index ec1dc0a77118..87b16efc64fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1574,7 +1574,7 @@ public class NotificationStackScrollLayout if (mMaxDisplayedNotifications != -1) { // The stack intrinsic height already contains the correct value when there is a limit // in the max number of notifications (e.g. as in keyguard). - height = mIntrinsicContentHeight; + height = mScrollViewFields.getIntrinsicStackHeight(); } else { height = Math.max(0f, mAmbientState.getStackCutoff() - mAmbientState.getStackTop()); } @@ -2610,7 +2610,7 @@ public class NotificationStackScrollLayout } @VisibleForTesting - void updateStackHeight() { + void updateIntrinsicStackHeight() { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; final int shelfIntrinsicHeight = mShelf != null ? mShelf.getIntrinsicHeight() : 0; @@ -2621,8 +2621,11 @@ public class NotificationStackScrollLayout mMaxDisplayedNotifications, shelfIntrinsicHeight ); - mIntrinsicContentHeight = notificationsHeight; - final int fullStackHeight = notificationsHeight + footerIntrinsicHeight + mBottomPadding; + // When there is a limit in the max number of notifications, we never display the footer. + final int fullStackHeight = mMaxDisplayedNotifications != -1 + ? notificationsHeight + : notificationsHeight + footerIntrinsicHeight + mBottomPadding; + if (mScrollViewFields.getIntrinsicStackHeight() != fullStackHeight) { mScrollViewFields.setIntrinsicStackHeight(fullStackHeight); notifyStackHeightChangedListeners(); @@ -2631,7 +2634,7 @@ public class NotificationStackScrollLayout private void updateContentHeight() { if (SceneContainerFlag.isEnabled()) { - updateStackHeight(); + updateIntrinsicStackHeight(); return; } @@ -5348,7 +5351,12 @@ public class NotificationStackScrollLayout public void setMaxDisplayedNotifications(int maxDisplayedNotifications) { if (mMaxDisplayedNotifications != maxDisplayedNotifications) { mMaxDisplayedNotifications = maxDisplayedNotifications; - updateContentHeight(); + if (SceneContainerFlag.isEnabled()) { + updateIntrinsicStackHeight(); + updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction()); + } else { + updateContentHeight(); + } notifyHeightChangeListener(mShelf); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 574ca3ffe5b6..56b335648138 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -104,7 +104,7 @@ constructor( 1f } else if ( change.isTransitioningBetween(Scenes.Gone, Scenes.Shade) || - change.isTransitioning(from = Scenes.Gone, to = Scenes.Lockscreen) + change.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) ) { shadeExpansion } else if (change.isTransitioningBetween(Scenes.Gone, Scenes.QuickSettings)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index cd59d4ebfd73..be2fb68ab88d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -58,6 +58,7 @@ import com.android.systemui.shade.ShadeViewStateProvider; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore; import com.android.systemui.statusbar.disableflags.DisableStateTracker; import com.android.systemui.statusbar.events.SystemStatusAnimationCallback; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; @@ -300,7 +301,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat KeyguardStatusBarViewModel keyguardStatusBarViewModel, BiometricUnlockController biometricUnlockController, SysuiStatusBarStateController statusBarStateController, - StatusBarContentInsetsProvider statusBarContentInsetsProvider, + StatusBarContentInsetsProviderStore statusBarContentInsetsProviderStore, UserManager userManager, StatusBarUserChipViewModel userChipViewModel, SecureSettings secureSettings, @@ -327,7 +328,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mKeyguardStatusBarViewModel = keyguardStatusBarViewModel; mBiometricUnlockController = biometricUnlockController; mStatusBarStateController = statusBarStateController; - mInsetsProvider = statusBarContentInsetsProvider; + mInsetsProvider = statusBarContentInsetsProviderStore.getDefaultDisplay(); mUserManager = userManager; mStatusBarUserChipViewModel = userChipViewModel; mSecureSettings = secureSettings; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index ff7c14308fcb..746d6a75a567 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -38,6 +38,7 @@ import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.window.StatusBarWindowStateController @@ -333,7 +334,7 @@ private constructor( private val configurationController: ConfigurationController, private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory, private val darkIconDispatcher: DarkIconDispatcher, - private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider, + private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, ) { fun create(view: PhoneStatusBarView): PhoneStatusBarViewController { val statusBarMoveFromCenterAnimationController = @@ -359,7 +360,7 @@ private constructor( configurationController, statusOverlayHoverListenerFactory, darkIconDispatcher, - statusBarContentInsetsProvider, + statusBarContentInsetsProviderStore.defaultDisplay, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt index c6f6bd90fce6..d991b1df45fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt @@ -24,6 +24,7 @@ import android.graphics.Point import android.graphics.Rect import android.util.LruCache import android.util.Pair +import android.view.Display.DEFAULT_DISPLAY import android.view.DisplayCutout import android.view.Surface import androidx.annotation.VisibleForTesting @@ -37,7 +38,7 @@ import com.android.systemui.SysUICutoutProvider import com.android.systemui.dump.DumpManager import com.android.systemui.res.R import com.android.systemui.statusbar.commandline.CommandRegistry -import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl.CacheKey +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE @@ -70,6 +71,17 @@ interface StatusBarContentInsetsProvider : CallbackController<StatusBarContentInsetsChangedListener> { /** + * Called when the [StatusBarContentInsetsProvider] should start doing its work and allocate its + * resources. + */ + fun start() + + /** + * Called when the [StatusBarContentInsetsProvider] should stop and do any required clean up. + */ + fun stop() + + /** * Some views may need to care about whether or not the current top display cutout is located in * the corner rather than somewhere in the center. In the case of a corner cutout, the status * bar area is contiguous. @@ -157,10 +169,15 @@ constructor( context.resources.getBoolean(R.bool.config_enablePrivacyDot) } - init { + private val nameSuffix = + if (context.displayId == DEFAULT_DISPLAY) "" else context.displayId.toString() + private val dumpableName = TAG + nameSuffix + private val commandName = StatusBarInsetsCommand.NAME + nameSuffix + + override fun start() { configurationController.addCallback(this) - dumpManager.registerDumpable(TAG, this) - commandRegistry.registerCommand(StatusBarInsetsCommand.NAME) { + dumpManager.registerDumpable(dumpableName, this) + commandRegistry.registerCommand(commandName) { StatusBarInsetsCommand( object : StatusBarInsetsCommand.Callback { override fun onExecute( @@ -174,6 +191,13 @@ constructor( } } + override fun stop() { + StatusBarConnectedDisplays.assertInNewMode() + configurationController.removeCallback(this) + dumpManager.unregisterDumpable(dumpableName) + commandRegistry.unregisterCommand(commandName) + } + override fun addCallback(listener: StatusBarContentInsetsChangedListener) { listeners.add(listener) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index f7fea7b0d334..92b609e87a90 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -56,6 +56,7 @@ import com.android.keyguard.TrustGrantFlags; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.DejankUtils; import com.android.systemui.Flags; +import com.android.systemui.animation.back.FlingOnBackAnimationCallback; import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor; @@ -236,9 +237,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } }; - private final OnBackAnimationCallback mOnBackInvokedCallback = new OnBackAnimationCallback() { + private final OnBackAnimationCallback mOnBackInvokedCallback = + new FlingOnBackAnimationCallback() { @Override - public void onBackInvoked() { + public void onBackInvokedCompat() { if (DEBUG) { Log.d(TAG, "onBackInvokedCallback() called, invoking onBackPressed()"); } @@ -249,21 +251,21 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } @Override - public void onBackProgressed(BackEvent event) { + public void onBackProgressedCompat(@NonNull BackEvent event) { if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) { mPrimaryBouncerView.getDelegate().getBackCallback().onBackProgressed(event); } } @Override - public void onBackCancelled() { + public void onBackCancelledCompat() { if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) { mPrimaryBouncerView.getDelegate().getBackCallback().onBackCancelled(); } } @Override - public void onBackStarted(BackEvent event) { + public void onBackStartedCompat(@NonNull BackEvent event) { if (shouldPlayBackAnimation() && mPrimaryBouncerView.getDelegate() != null) { mPrimaryBouncerView.getDelegate().getBackCallback().onBackStarted(event); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt index ae0e76f01faa..584cd3b00a57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt @@ -23,6 +23,7 @@ import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import java.util.Optional /** Encapsulates all logic for the status bar window state management. */ @@ -82,6 +83,7 @@ interface StatusBarWindowController { context: Context, viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, statusBarConfigurationController: StatusBarConfigurationController, + contentInsetsProvider: StatusBarContentInsetsProvider, ): StatusBarWindowController } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java index e4c6737856f0..6953bbf735f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java @@ -99,7 +99,7 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController @Assisted ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, @Assisted StatusBarConfigurationController statusBarConfigurationController, IWindowManager iWindowManager, - StatusBarContentInsetsProvider contentInsetsProvider, + @Assisted StatusBarContentInsetsProvider contentInsetsProvider, FragmentService fragmentService, Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider) { mContext = context; @@ -370,7 +370,8 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController StatusBarWindowControllerImpl create( @NonNull Context context, @NonNull ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, - @NonNull StatusBarConfigurationController statusBarConfigurationController); + @NonNull StatusBarConfigurationController statusBarConfigurationController, + @NonNull StatusBarContentInsetsProvider contentInsetsProvider); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt index d83a2371ec92..051d463a8b97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt @@ -28,6 +28,7 @@ import com.android.systemui.display.data.repository.PerDisplayStoreImpl import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerStore +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -43,6 +44,7 @@ constructor( private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository, private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory, private val statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore, + private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, displayRepository: DisplayRepository, ) : StatusBarWindowControllerStore, @@ -64,6 +66,7 @@ constructor( statusBarDisplayContext.context, viewCaptureAwareWindowManager, statusBarConfigurationControllerStore.forDisplay(displayId), + statusBarContentInsetsProviderStore.forDisplay(displayId), ) } @@ -78,6 +81,7 @@ constructor( viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, factory: StatusBarWindowControllerImpl.Factory, statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore, + statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, ) : StatusBarWindowControllerStore, PerDisplayStore<StatusBarWindowController> by SingleDisplayStore( @@ -85,6 +89,7 @@ constructor( context, viewCaptureAwareWindowManager, statusBarConfigurationControllerStore.defaultDisplay, + statusBarContentInsetsProviderStore.defaultDisplay, ) ) { 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 618722a79a6f..2337ec1801d7 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 @@ -44,11 +44,7 @@ fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni titleSuccessResId = R.string.touchpad_back_gesture_success_title, bodySuccessResId = R.string.touchpad_back_gesture_success_body, ), - animations = - TutorialScreenConfig.Animations( - educationResId = R.raw.trackpad_back_edu, - successResId = R.raw.trackpad_back_success, - ), + animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_back_edu), ) val recognizer = rememberBackGestureRecognizer(LocalContext.current.resources) val gestureUiState: Flow<GestureUiState> = 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 11e1ff49074a..e058527ffdbe 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 @@ -74,7 +74,7 @@ fun GestureUiState.toTutorialActionState(): TutorialActionState { NotStarted -> TutorialActionState.NotStarted // progress is disabled for now as views are not ready to handle varying progress is GestureUiState.InProgress -> TutorialActionState.InProgress(progress = 0f) - is Finished -> TutorialActionState.Finished + is Finished -> TutorialActionState.Finished(successAnimation) } } 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 05871ce91e39..55749b2f3656 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 @@ -43,11 +43,7 @@ fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni titleSuccessResId = R.string.touchpad_home_gesture_success_title, bodySuccessResId = R.string.touchpad_home_gesture_success_body, ), - animations = - TutorialScreenConfig.Animations( - educationResId = R.raw.trackpad_home_edu, - successResId = R.raw.trackpad_home_success, - ), + animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_home_edu), ) val recognizer = rememberHomeGestureRecognizer(LocalContext.current.resources) val gestureUiState: Flow<GestureUiState> = 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 4fd16448e9f2..d928535f721f 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 @@ -44,10 +44,7 @@ fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body, ), animations = - TutorialScreenConfig.Animations( - educationResId = R.raw.trackpad_recent_apps_edu, - successResId = R.raw.trackpad_recent_apps_success, - ), + TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_recent_apps_edu), ) val recognizer = rememberRecentAppsGestureRecognizer(LocalContext.current.resources) val gestureUiState: Flow<GestureUiState> = 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 024048cbbb24..35ea0ea3e048 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 @@ -18,6 +18,9 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.util.MathUtils import android.view.MotionEvent +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection.LEFT +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection.RIGHT +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress import kotlin.math.abs /** @@ -44,7 +47,13 @@ class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu gestureStateChangedCallback, gestureState, isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx }, - progress = { MathUtils.saturate(abs(it.deltaX / gestureDistanceThresholdPx)) }, + progress = ::getProgress, ) } + + private fun getProgress(it: Moving): InProgress { + val direction = if (it.deltaX > 0) RIGHT else LEFT + val value = MathUtils.saturate(abs(it.deltaX / gestureDistanceThresholdPx)) + return InProgress(value, direction) + } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt index b513c49371a4..f27ddb515c24 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt @@ -21,5 +21,11 @@ sealed interface GestureState { data object Finished : GestureState - data class InProgress(val progress: Float = 0f) : GestureState + data class InProgress(val progress: Float = 0f, val direction: GestureDirection? = null) : + GestureState +} + +enum class GestureDirection { + LEFT, + RIGHT, } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt index f19467726def..24f5d1f00794 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt @@ -21,7 +21,7 @@ inline fun updateGestureState( gestureStateChangedCallback: (GestureState) -> Unit, gestureState: DistanceGestureState?, isFinished: (Finished) -> Boolean, - progress: (Moving) -> Float, + progress: (Moving) -> GestureState.InProgress, ) { when (gestureState) { is Finished -> { @@ -32,7 +32,7 @@ inline fun updateGestureState( } } is Moving -> { - gestureStateChangedCallback(GestureState.InProgress(progress(gestureState))) + gestureStateChangedCallback(progress(gestureState)) } is Started -> gestureStateChangedCallback(GestureState.InProgress()) else -> {} 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 b804b9a0d4c7..e10b8253ebad 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 @@ -18,6 +18,7 @@ package com.android.systemui.touchpad.tutorial.ui.gesture import android.util.MathUtils import android.view.MotionEvent +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress /** Recognizes touchpad home gesture, that is - using three fingers on touchpad - swiping up. */ class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer { @@ -40,7 +41,7 @@ class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu gestureStateChangedCallback, gestureState, isFinished = { -it.deltaY >= gestureDistanceThresholdPx }, - progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) }, + progress = { InProgress(MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx)) }, ) } } 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 7d484ee85b7c..c47888609a61 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 @@ -54,7 +54,9 @@ class RecentAppsGestureRecognizer( -state.deltaY >= gestureDistanceThresholdPx && abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs }, - progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) }, + progress = { + GestureState.InProgress(MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx)) + }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt index 4eae3b9a8da5..c7f5801a87c2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt @@ -17,8 +17,11 @@ package com.android.systemui.volume.dialog.ui.utils import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator import android.view.ViewPropertyAnimator import kotlin.coroutines.resume +import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine /** @@ -39,11 +42,12 @@ suspend fun ViewPropertyAnimator.suspendAnimate( } override fun onAnimationEnd(animation: Animator) { - continuation.resume(Unit) + continuation.resumeIfCan(Unit) animationListener?.onAnimationEnd(animation) } override fun onAnimationCancel(animation: Animator) { + continuation.resumeIfCan(Unit) animationListener?.onAnimationCancel(animation) } @@ -54,3 +58,30 @@ suspend fun ViewPropertyAnimator.suspendAnimate( ) continuation.invokeOnCancellation { this.cancel() } } + +/** + * Starts animation and suspends until it's finished. Cancels the animation if the running coroutine + * is cancelled. + */ +@Suppress("UNCHECKED_CAST") +suspend fun <T> ValueAnimator.awaitAnimation(onValueChanged: (T) -> Unit) { + suspendCancellableCoroutine { continuation -> + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) = continuation.resumeIfCan(Unit) + + override fun onAnimationCancel(animation: Animator) = continuation.resumeIfCan(Unit) + } + ) + addUpdateListener { onValueChanged(it.animatedValue as T) } + + start() + continuation.invokeOnCancellation { cancel() } + } +} + +private fun <T> CancellableContinuation<T>.resumeIfCan(value: T) { + if (!isCancelled && !isCompleted) { + resume(value) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt index fc2ad60cfa72..eae828562223 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt @@ -53,7 +53,7 @@ import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CON import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT import com.android.systemui.shade.carrier.ShadeCarrierGroup import com.android.systemui.shade.carrier.ShadeCarrierGroupController -import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider +import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory import com.android.systemui.statusbar.phone.ui.StatusBarIconController @@ -63,6 +63,7 @@ import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.NextAlarmController import com.android.systemui.statusbar.policy.VariableDateView import com.android.systemui.statusbar.policy.VariableDateViewController +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture @@ -93,6 +94,10 @@ private val EMPTY_CHANGES = ConstraintsChanges() @RunWith(AndroidTestingRunner::class) class ShadeHeaderControllerTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val insetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore + private val insetsProvider = insetsProviderStore.defaultDisplay + @Mock(answer = Answers.RETURNS_MOCKS) private lateinit var view: MotionLayout @Mock private lateinit var statusIcons: StatusIconContainer @Mock private lateinit var statusBarIconController: StatusBarIconController @@ -107,7 +112,6 @@ class ShadeHeaderControllerTest : SysuiTestCase() { @Mock private lateinit var batteryMeterView: BatteryMeterView @Mock private lateinit var batteryMeterViewController: BatteryMeterViewController @Mock private lateinit var privacyIconsController: HeaderPrivacyIconsController - @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider @Mock private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory @Mock private lateinit var variableDateViewController: VariableDateViewController @Mock private lateinit var dumpManager: DumpManager @@ -190,7 +194,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { statusBarIconController, iconManagerFactory, privacyIconsController, - insetsProvider, + insetsProviderStore, configurationController, variableDateViewControllerFactory, batteryMeterViewController, @@ -201,7 +205,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { qsBatteryModeController, nextAlarmController, activityStarter, - mStatusOverlayHoverListenerFactory + mStatusOverlayHoverListenerFactory, ) whenever(view.isAttachedToWindow).thenReturn(true) shadeHeaderController.init() @@ -597,7 +601,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { anyInt(), anyInt(), anyInt(), - anyInt() + anyInt(), ) ) .thenReturn(mockConstraintsChanges) @@ -631,7 +635,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { anyInt(), anyInt(), anyInt(), - anyInt() + anyInt(), ) ) .thenReturn(mockConstraintsChanges) @@ -751,7 +755,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { whenever( combinedShadeHeadersConstraintManager.centerCutoutConstraints( Mockito.anyBoolean(), - anyInt() + anyInt(), ) ) .thenReturn(mockConstraintsChanges) @@ -788,7 +792,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { whenever( combinedShadeHeadersConstraintManager.centerCutoutConstraints( Mockito.anyBoolean(), - anyInt() + anyInt(), ) ) .thenReturn(mockConstraintsChanges) @@ -899,7 +903,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { top: Int, right: Int, bottom: Int, - listener: View.OnLayoutChangeListener + listener: View.OnLayoutChangeListener, ) { val oldLeft = this.left val oldTop = this.top @@ -920,7 +924,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { left, top, right, - bottom + bottom, ) } @@ -941,7 +945,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { /* left= */ insets.first, /* top= */ 0, /* right= */ insets.second, - /* bottom= */ 0 + /* bottom= */ 0, ) ) whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(cornerCutout) @@ -968,7 +972,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { anyInt(), anyInt(), anyInt(), - anyInt() + anyInt(), ) ) .thenReturn(EMPTY_CHANGES) @@ -977,7 +981,7 @@ class ShadeHeaderControllerTest : SysuiTestCase() { whenever( combinedShadeHeadersConstraintManager.centerCutoutConstraints( Mockito.anyBoolean(), - anyInt() + anyInt(), ) ) .thenReturn(EMPTY_CHANGES) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 87cda64ed8e5..fdfc25390a3f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -244,7 +244,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { when(mStackSizeCalculator.computeHeight(eq(mStackScroller), anyInt(), anyFloat())) .thenReturn((float) stackHeight); - mStackScroller.updateStackHeight(); + mStackScroller.updateIntrinsicStackHeight(); assertThat(mStackScroller.getIntrinsicStackHeight()).isEqualTo(stackHeight); } @@ -1218,7 +1218,38 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableSceneContainer // TODO(b/312473478): address disabled test + @EnableSceneContainer + public void testSetMaxDisplayedNotifications_updatesStackHeight() { + int fullStackHeight = 300; + int limitedStackHeight = 100; + int maxNotifs = 2; // any non-zero limit + float stackTop = 100; + float stackCutoff = 1100; + float stackViewPortHeight = stackCutoff - stackTop; + mStackScroller.setStackTop(stackTop); + mStackScroller.setStackCutoff(stackCutoff); + when(mStackSizeCalculator.computeHeight(eq(mStackScroller), eq(-1), anyFloat())) + .thenReturn((float) fullStackHeight); + when(mStackSizeCalculator.computeHeight(eq(mStackScroller), eq(maxNotifs), anyFloat())) + .thenReturn((float) limitedStackHeight); + + // When we set a limit on max displayed notifications + mStackScroller.setMaxDisplayedNotifications(maxNotifs); + + // Then + assertThat(mStackScroller.getIntrinsicStackHeight()).isEqualTo(limitedStackHeight); + assertThat(mAmbientState.getStackEndHeight()).isEqualTo(limitedStackHeight); + + // When there is no limit on max displayed notifications + mStackScroller.setMaxDisplayedNotifications(-1); + + // Then + assertThat(mStackScroller.getIntrinsicStackHeight()).isEqualTo(fullStackHeight); + assertThat(mAmbientState.getStackEndHeight()).isEqualTo(stackViewPortHeight); + } + + @Test + @DisableSceneContainer public void testSetMaxDisplayedNotifications_notifiesListeners() { ExpandableView.OnHeightChangedListener listener = mock(ExpandableView.OnHeightChangedListener.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 83d0bcc55cb0..008e8ce92736 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.battery.BatteryMeterView import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.fakeDarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView @@ -46,9 +45,12 @@ import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.data.repository.statusBarContentInsetsProviderStore import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.window.StatusBarWindowStateController +import com.android.systemui.testKosmos import com.android.systemui.unfold.SysUIUnfoldComponent import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider @@ -75,7 +77,9 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class PhoneStatusBarViewControllerTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = testKosmos() + private val statusBarContentInsetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore + private val statusBarContentInsetsProvider = statusBarContentInsetsProviderStore.defaultDisplay private val fakeDarkIconDispatcher = kosmos.fakeDarkIconDispatcher @Mock private lateinit var shadeViewController: ShadeViewController @@ -93,7 +97,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var windowRootView: Provider<WindowRootView> @Mock private lateinit var shadeLogger: ShadeLogger @Mock private lateinit var viewUtil: ViewUtil - @Mock private lateinit var statusBarContentInsetsProvider: StatusBarContentInsetsProvider private lateinit var statusBarWindowStateController: StatusBarWindowStateController private lateinit var view: PhoneStatusBarView @@ -396,7 +399,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { configurationController, mStatusOverlayHoverListenerFactory, fakeDarkIconDispatcher, - statusBarContentInsetsProvider, + statusBarContentInsetsProviderStore, ) .create(view) .also { it.init() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/CameraProtectionLoaderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/CameraProtectionLoaderKosmos.kt new file mode 100644 index 000000000000..7625049594a4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/CameraProtectionLoaderKosmos.kt @@ -0,0 +1,25 @@ +/* + * 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 + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeCameraProtectionLoaderFactory by + Kosmos.Fixture { FakeCameraProtectionLoaderFactory() } + +var Kosmos.cameraProtectionLoaderFactory: CameraProtectionLoaderImpl.Factory by + Kosmos.Fixture { fakeCameraProtectionLoaderFactory } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeCameraProtectionLoaderFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeCameraProtectionLoaderFactory.kt new file mode 100644 index 000000000000..87428b0679ba --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeCameraProtectionLoaderFactory.kt @@ -0,0 +1,27 @@ +/* + * 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 + +import android.content.Context +import org.mockito.kotlin.mock + +class FakeCameraProtectionLoaderFactory : CameraProtectionLoaderImpl.Factory { + + override fun create(context: Context): CameraProtectionLoaderImpl { + return mock<CameraProtectionLoaderImpl>() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSysUICutoutProviderFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSysUICutoutProviderFactory.kt new file mode 100644 index 000000000000..4eb3780d7663 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSysUICutoutProviderFactory.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 + +import android.content.Context +import org.mockito.kotlin.mock + +class FakeSysUICutoutProviderFactory : SysUICutoutProviderImpl.Factory { + + override fun create( + context: Context, + cameraProtectionLoader: CameraProtectionLoader, + ): SysUICutoutProviderImpl { + return mock<SysUICutoutProviderImpl>() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUICutoutProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUICutoutProviderKosmos.kt new file mode 100644 index 000000000000..412ed72e89f7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUICutoutProviderKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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 + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeSysUICutoutProviderFactory by Kosmos.Fixture { FakeSysUICutoutProviderFactory() } + +var Kosmos.sysUICutoutProviderFactory: SysUICutoutProviderImpl.Factory by + Kosmos.Fixture { fakeSysUICutoutProviderFactory } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarConfigurationControllerStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarConfigurationControllerStore.kt new file mode 100644 index 000000000000..46bb2c169731 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarConfigurationControllerStore.kt @@ -0,0 +1,32 @@ +/* + * 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.data.repository + +import android.view.Display +import org.mockito.kotlin.mock + +class FakeStatusBarConfigurationControllerStore : StatusBarConfigurationControllerStore { + + private val perDisplayMockControllers = mutableMapOf<Int, StatusBarConfigurationController>() + + override val defaultDisplay: StatusBarConfigurationController + get() = forDisplay(Display.DEFAULT_DISPLAY) + + override fun forDisplay(displayId: Int): StatusBarConfigurationController { + return perDisplayMockControllers.computeIfAbsent(displayId) { mock() } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt new file mode 100644 index 000000000000..642c2ff38338 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.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.statusbar.data.repository + +import android.view.Display +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider +import org.mockito.kotlin.mock + +class FakeStatusBarContentInsetsProviderStore() : StatusBarContentInsetsProviderStore { + + private val perDisplayMockProviders = mutableMapOf<Int, StatusBarContentInsetsProvider>() + + override val defaultDisplay: StatusBarContentInsetsProvider + get() = forDisplay(Display.DEFAULT_DISPLAY) + + override fun forDisplay(displayId: Int): StatusBarContentInsetsProvider { + return perDisplayMockProviders.computeIfAbsent(displayId) { mock() } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationControllerStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationControllerStoreKosmos.kt new file mode 100644 index 000000000000..03b63c2a8757 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarConfigurationControllerStoreKosmos.kt @@ -0,0 +1,26 @@ +/* + * 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.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeStatusBarConfigurationControllerStore by Kosmos.Fixture { + FakeStatusBarConfigurationControllerStore() +} + +var Kosmos.statusBarConfigurationControllerStore: StatusBarConfigurationControllerStore by + Kosmos.Fixture { fakeStatusBarConfigurationControllerStore } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt new file mode 100644 index 000000000000..a34fb0998c79 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt @@ -0,0 +1,44 @@ +/* + * 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.data.repository + +import com.android.systemui.cameraProtectionLoaderFactory +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.display.data.repository.displayWindowPropertiesRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.phone.statusBarContentInsetsProviderFactory +import com.android.systemui.sysUICutoutProviderFactory + +val Kosmos.fakeStatusBarContentInsetsProviderStore by + Kosmos.Fixture { FakeStatusBarContentInsetsProviderStore() } + +val Kosmos.multiDisplayStatusBarContentInsetsProviderStore by + Kosmos.Fixture { + MultiDisplayStatusBarContentInsetsProviderStore( + applicationCoroutineScope, + displayRepository, + statusBarContentInsetsProviderFactory, + displayWindowPropertiesRepository, + statusBarConfigurationControllerStore, + sysUICutoutProviderFactory, + cameraProtectionLoaderFactory, + ) + } + +var Kosmos.statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore by + Kosmos.Fixture { fakeStatusBarContentInsetsProviderStore } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt new file mode 100644 index 000000000000..4fb8cf4a328b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.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.statusbar.phone + +import android.content.Context +import com.android.systemui.SysUICutoutProvider +import com.android.systemui.statusbar.policy.ConfigurationController +import org.mockito.kotlin.mock + +class FakeStatusBarContentInsetsProviderFactory : StatusBarContentInsetsProviderImpl.Factory { + + override fun create( + context: Context, + configurationController: ConfigurationController, + sysUICutoutProvider: SysUICutoutProvider, + ): StatusBarContentInsetsProviderImpl { + return mock<StatusBarContentInsetsProviderImpl>() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt index 9c9673c3a924..705df3c15d18 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt @@ -23,3 +23,9 @@ val Kosmos.mockStatusBarContentInsetsProvider by Kosmos.Fixture { mock<StatusBarContentInsetsProvider>() } var Kosmos.statusBarContentInsetsProvider by Kosmos.Fixture { mockStatusBarContentInsetsProvider } + +val Kosmos.fakeStatusBarContentInsetsProviderFactory by + Kosmos.Fixture { FakeStatusBarContentInsetsProviderFactory() } + +var Kosmos.statusBarContentInsetsProviderFactory: StatusBarContentInsetsProviderImpl.Factory by + Kosmos.Fixture { fakeStatusBarContentInsetsProviderFactory } 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 65247a55348d..7eaecb1c4544 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 @@ -19,11 +19,13 @@ package com.android.systemui.statusbar.window import android.content.Context import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory { override fun create( context: Context, viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, statusBarConfigurationController: StatusBarConfigurationController, + contentInsetsProvider: StatusBarContentInsetsProvider, ) = FakeStatusBarWindowController() } diff --git a/services/core/java/com/android/server/TradeInModeService.java b/services/core/java/com/android/server/TradeInModeService.java new file mode 100644 index 000000000000..9ad550b6caf9 --- /dev/null +++ b/services/core/java/com/android/server/TradeInModeService.java @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server; + +import static com.android.tradeinmode.flags.Flags.enableTradeInMode; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.OnAccountsUpdateListener; +import android.annotation.RequiresPermission; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.Uri; +import android.os.Binder; +import android.os.ITradeInMode; +import android.os.SystemProperties; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.service.persistentdata.PersistentDataBlockManager; +import android.util.Slog; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + + +public final class TradeInModeService extends SystemService { + private static final String TAG = "TradeInModeService"; + + private static final String TIM_PROP = "persist.adb.tradeinmode"; + + private static final int TIM_STATE_UNSET = 0; + + // adbd_tradeinmode was stopped. + private static final int TIM_STATE_DISABLED = -1; + + // adbd_tradeinmode has started. + private static final int TIM_STATE_FOYER = 1; + + // Full non-root adb granted; factory reset is guaranteed. + private static final int TIM_STATE_EVALUATION_MODE = 2; + + // This file contains a single integer counter of how many boot attempts + // have been made since entering evaluation mode. + private static final String WIPE_INDICATOR_FILE = "/metadata/tradeinmode/wipe"; + + private final Context mContext; + private TradeInMode mTradeInMode; + + private ConnectivityManager mConnectivityManager; + private ConnectivityManager.NetworkCallback mNetworkCallback = null; + + private AccountManager mAccountManager; + private OnAccountsUpdateListener mAccountsListener = null; + + public TradeInModeService(Context context) { + super(context); + + mContext = context; + } + + @Override + public void onStart() { + if (!enableTradeInMode()) { + return; + } + + mTradeInMode = new TradeInMode(); + publishBinderService("tradeinmode", mTradeInMode); + } + + @Override + public void onBootPhase(@BootPhase int phase) { + if (phase == PHASE_SYSTEM_SERVICES_READY) { + final int state = getTradeInModeState(); + + if (isAdbEnabled() && !isDebuggable() && !isDeviceSetup() + && state == TIM_STATE_DISABLED) { + // If we fail to start trade-in mode, the persist property may linger + // past reboot. If we detect this, disable ADB and clear TIM state. + Slog.i(TAG, "Resetting trade-in mode state."); + SystemProperties.set(TIM_PROP, ""); + + final ContentResolver cr = mContext.getContentResolver(); + Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 0); + } else if (state == TIM_STATE_FOYER) { + // If zygote crashed or we rebooted, and TIM is still enabled, make + // sure it's allowed to be enabled. If it is, we need to re-add our + // setup completion observer. + if (isDeviceSetup()) { + stopTradeInMode(); + } else { + watchForSetupCompletion(); + } + } + } + } + + private final class TradeInMode extends ITradeInMode.Stub { + @Override + @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) + public boolean start() { + mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE", + "Cannot enter trade-in mode foyer"); + final int state = getTradeInModeState(); + if (state == TIM_STATE_FOYER) { + return true; + } + + if (state != TIM_STATE_UNSET) { + Slog.e(TAG, "Cannot enter trade-in mode in state: " + state); + return false; + } + + if (isDeviceSetup()) { + Slog.i(TAG, "Not starting trade-in mode, device is setup."); + return false; + } + if (SystemProperties.getInt("ro.debuggable", 0) == 1) { + // We don't want to force adbd into TIM on debug builds. + Slog.e(TAG, "Not starting trade-in mode, device is debuggable."); + return false; + } + if (isAdbEnabled()) { + Slog.e(TAG, "Not starting trade-in mode, adb is already enabled."); + return false; + } + + final long callingId = Binder.clearCallingIdentity(); + try { + startTradeInMode(); + } finally { + Binder.restoreCallingIdentity(callingId); + } + return true; + } + + @Override + @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) + public boolean enterEvaluationMode() { + mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE", + "Cannot enter trade-in evaluation mode"); + final int state = getTradeInModeState(); + if (state != TIM_STATE_FOYER) { + Slog.e(TAG, "Cannot enter evaluation mode in state: " + state); + return false; + } + if (isFrpActive()) { + Slog.e(TAG, "Cannot enter evaluation mode, FRP lock is present."); + return false; + } + + try (FileWriter fw = new FileWriter(WIPE_INDICATOR_FILE, + StandardCharsets.US_ASCII)) { + fw.write("0"); + } catch (IOException e) { + Slog.e(TAG, "Failed to write " + WIPE_INDICATOR_FILE, e); + return false; + } + + final long callingId = Binder.clearCallingIdentity(); + try { + removeNetworkWatch(); + removeAccountsWatch(); + } finally { + Binder.restoreCallingIdentity(callingId); + } + + SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_EVALUATION_MODE)); + SystemProperties.set("ctl.restart", "adbd"); + return true; + } + + @Override + @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE) + public boolean isEvaluationModeAllowed() { + mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE", + "Cannot test for trade-in evaluation mode allowed"); + return !isFrpActive(); + } + } + + private void startTradeInMode() { + Slog.i(TAG, "Enabling trade-in mode."); + + SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_FOYER)); + + final ContentResolver cr = mContext.getContentResolver(); + Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 1); + + watchForSetupCompletion(); + watchForNetworkChange(); + watchForAccountsCreated(); + } + + private void stopTradeInMode() { + Slog.i(TAG, "Stopping trade-in mode."); + + SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_DISABLED)); + + removeNetworkWatch(); + removeAccountsWatch(); + + final ContentResolver cr = mContext.getContentResolver(); + Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 0); + } + + private int getTradeInModeState() { + return SystemProperties.getInt(TIM_PROP, TIM_STATE_UNSET); + } + + private boolean isDebuggable() { + return SystemProperties.getInt("ro.debuggable", 0) == 1; + } + + private boolean isAdbEnabled() { + final ContentResolver cr = mContext.getContentResolver(); + return Settings.Global.getInt(cr, Settings.Global.ADB_ENABLED, 0) == 1; + } + + private boolean isFrpActive() { + try { + PersistentDataBlockManager pdb = + mContext.getSystemService(PersistentDataBlockManager.class); + if (pdb == null) { + return false; + } + return pdb.isFactoryResetProtectionActive(); + } catch (Exception e) { + Slog.e(TAG, "Could not read PDB", e); + return false; + } + } + + // This returns true if the device has progressed far enough into Setup Wizard that it no + // longer makes sense to enable trade-in mode. As a last stop, we check the SUW completion + // bits. + private boolean isDeviceSetup() { + final ContentResolver cr = mContext.getContentResolver(); + try { + if (Settings.Secure.getIntForUser(cr, Settings.Secure.USER_SETUP_COMPLETE, 0) != 0) { + return true; + } + } catch (SettingNotFoundException e) { + Slog.e(TAG, "Could not find USER_SETUP_COMPLETE setting", e); + } + + if (Settings.Global.getInt(cr, Settings.Global.DEVICE_PROVISIONED, 0) != 0) { + return true; + } + return false; + } + + private final class SettingsObserver extends ContentObserver { + SettingsObserver() { + super(null); + } + + @Override + public void onChange(boolean selfChange) { + if (getTradeInModeState() == TIM_STATE_FOYER && isDeviceSetup()) { + stopTradeInMode(); + } + } + } + + private void watchForSetupCompletion() { + final Uri userSetupComplete = Settings.Secure.getUriFor( + Settings.Secure.USER_SETUP_COMPLETE); + final Uri deviceProvisioned = Settings.Global.getUriFor( + Settings.Global.DEVICE_PROVISIONED); + final ContentResolver cr = mContext.getContentResolver(); + final SettingsObserver observer = new SettingsObserver(); + + cr.registerContentObserver(userSetupComplete, false, observer); + cr.registerContentObserver(deviceProvisioned, false, observer); + } + + + private void watchForNetworkChange() { + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + NetworkRequest networkRequest = new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(); + + mNetworkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + super.onAvailable(network); + stopTradeInMode(); + } + }; + + mConnectivityManager.registerNetworkCallback(networkRequest, mNetworkCallback); + } + + private void removeNetworkWatch() { + if (mNetworkCallback != null) { + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + mNetworkCallback = null; + } + } + + private void watchForAccountsCreated() { + mAccountManager = mContext.getSystemService(AccountManager.class); + mAccountsListener = new OnAccountsUpdateListener() { + @Override + public void onAccountsUpdated(Account[] accounts) { + stopTradeInMode(); + } + }; + mAccountManager.addOnAccountsUpdatedListener(mAccountsListener, null, false); + } + + private void removeAccountsWatch() { + if (mAccountsListener != null) { + mAccountManager.removeOnAccountsUpdatedListener(mAccountsListener); + mAccountsListener = null; + } + } +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 6ba851423219..a6189d2148fa 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -632,8 +632,8 @@ public class ActivityManagerService extends IActivityManager.Stub static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; static final String EXTRA_BUGREPORT_TYPE = "android.intent.extra.BUGREPORT_TYPE"; static final String EXTRA_BUGREPORT_NONCE = "android.intent.extra.BUGREPORT_NONCE"; - static final String EXTRA_EXTRA_ATTACHMENT_URI = - "android.intent.extra.EXTRA_ATTACHMENT_URI"; + static final String EXTRA_EXTRA_ATTACHMENT_URIS = + "android.intent.extra.EXTRA_ATTACHMENT_URIS"; /** * The maximum number of bytes that {@link #setProcessStateSummary} accepts. @@ -7660,7 +7660,7 @@ public class ActivityManagerService extends IActivityManager.Stub */ public void requestBugReportWithDescription(@Nullable String shareTitle, @Nullable String shareDescription, int bugreportType, long nonce, - @Nullable Uri extraAttachment) { + @Nullable List<Uri> extraAttachments) { String type = null; switch (bugreportType) { case BugreportParams.BUGREPORT_MODE_FULL: @@ -7715,8 +7715,9 @@ public class ActivityManagerService extends IActivityManager.Stub triggerShellBugreport.setPackage(SHELL_APP_PACKAGE); triggerShellBugreport.putExtra(EXTRA_BUGREPORT_TYPE, bugreportType); triggerShellBugreport.putExtra(EXTRA_BUGREPORT_NONCE, nonce); - if (extraAttachment != null) { - triggerShellBugreport.putExtra(EXTRA_EXTRA_ATTACHMENT_URI, extraAttachment); + if (extraAttachments != null && !extraAttachments.isEmpty()) { + triggerShellBugreport.putParcelableArrayListExtra(EXTRA_EXTRA_ATTACHMENT_URIS, + new ArrayList(extraAttachments)); triggerShellBugreport.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } triggerShellBugreport.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); @@ -7775,9 +7776,9 @@ public class ActivityManagerService extends IActivityManager.Stub * Takes an interactive bugreport with a progress notification. Also attaches given file uri. */ @Override - public void requestBugReportWithExtraAttachment(@NonNull Uri extraAttachment) { + public void requestBugReportWithExtraAttachments(@NonNull List<Uri> extraAttachments) { requestBugReportWithDescription(null, null, BugreportParams.BUGREPORT_MODE_INTERACTIVE, 0L, - extraAttachment); + extraAttachments); } /** diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 78a1fa768879..bfef6855fe7e 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -68,10 +68,11 @@ import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; import static com.android.media.audio.Flags.replaceStreamBtSco; -import static com.android.media.audio.Flags.ringerModeAffectsAlarm; import static com.android.media.audio.Flags.ringMyCar; +import static com.android.media.audio.Flags.ringerModeAffectsAlarm; import static com.android.media.audio.Flags.setStreamVolumeOrder; import static com.android.media.audio.Flags.vgsVssSyncMuteOrder; +import static com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl; import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE; import static com.android.server.utils.EventLogger.Event.ALOGE; import static com.android.server.utils.EventLogger.Event.ALOGI; @@ -491,6 +492,10 @@ public class AudioService extends IAudioService.Stub private static final int MSG_INIT_SPATIALIZER = 102; private static final int MSG_INIT_ADI_DEVICE_STATES = 103; + private static final int MSG_INIT_INPUT_GAINS = 104; + private static final int MSG_SET_INPUT_GAIN_INDEX = 105; + private static final int MSG_PERSIST_INPUT_GAIN_INDEX = 106; + // end of messages handled under wakelock // retry delay in case of failure to indicate system ready to AudioFlinger @@ -512,6 +517,11 @@ public class AudioService extends IAudioService.Stub **/ private SparseArray<VolumeStreamState> mStreamStates; + /** + * @see InputDeviceVolumeHelper + */ + private InputDeviceVolumeHelper mInputDeviceVolumeHelper; + /*package*/ int getVssVolumeForDevice(int stream, int device) { final VolumeStreamState streamState = mStreamStates.get(stream); return streamState != null ? streamState.getIndex(device) : -1; @@ -1501,6 +1511,15 @@ public class AudioService extends IAudioService.Stub 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */); queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_SPATIALIZER, 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */); + if (enableAudioInputDeviceRoutingAndVolumeControl()) { + queueMsgUnderWakeLock( + mAudioHandler, + MSG_INIT_INPUT_GAINS, + 0 /* arg1 */, + 0 /* arg2 */, + null /* obj */, + 0 /* delay */); + } mDisplayManager = context.getSystemService(DisplayManager.class); @@ -1594,6 +1613,16 @@ public class AudioService extends IAudioService.Stub } } + /** Called by handling of MSG_INIT_INPUT_GAINS */ + private void onInitInputGains() { + mInputDeviceVolumeHelper = + new InputDeviceVolumeHelper( + mSettings, + mContentResolver, + mSettingsLock, + System.INPUT_GAIN_INDEX_SETTINGS); + } + private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener = new SubscriptionManager.OnSubscriptionsChangedListener() { @Override @@ -5742,6 +5771,90 @@ public class AudioService extends IAudioService.Stub : aliasStreamType == sStreamVolumeAlias.get(AudioSystem.STREAM_SYSTEM); } + /** + * @see AudioDeviceVolumeManager#setInputGainIndex(AudioDeviceAttributes, int) + */ + @Override + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public void setInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) { + super.setInputGainIndex_enforcePermission(); + + if (mInputDeviceVolumeHelper.setInputGainIndex(ada, index)) { + // Post message to set system volume (it in turn will post a message + // to persist). + sendMsg( + mAudioHandler, + MSG_SET_INPUT_GAIN_INDEX, + SENDMSG_QUEUE, + /*arg1*/ index, + /*arg2*/ 0, + /*obj*/ ada, + /*delay*/ 0); + } + } + + private void setInputGainIndexInt(@NonNull AudioDeviceAttributes ada, int index) { + // TODO(b/364923030): call AudioSystem to apply input gain in native layer. + + // Post a persist input gain msg. + sendMsg( + mAudioHandler, + MSG_PERSIST_INPUT_GAIN_INDEX, + SENDMSG_QUEUE, + /*arg1*/ index, + /*arg2*/ 0, + /*obj*/ ada, + PERSIST_DELAY); + } + + private void persistInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) { + mInputDeviceVolumeHelper.persistInputGainIndex(ada, index); + } + + /** + * @see AudioDeviceVolumeManager#getInputGainIndex(AudioDeviceAttributes) + */ + @Override + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int getInputGainIndex(@NonNull AudioDeviceAttributes ada) { + super.getInputGainIndex_enforcePermission(); + + return mInputDeviceVolumeHelper.getInputGainIndex(ada); + } + + /** + * @see AudioDeviceVolumeManager#getMaxInputGainIndex() + */ + @Override + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int getMaxInputGainIndex() { + super.getMaxInputGainIndex_enforcePermission(); + + return mInputDeviceVolumeHelper.getMaxInputGainIndex(); + } + + /** + * @see AudioDeviceVolumeManager#getMinInputGainIndex() + */ + @Override + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public int getMinInputGainIndex() { + super.getMinInputGainIndex_enforcePermission(); + + return mInputDeviceVolumeHelper.getMinInputGainIndex(); + } + + /** + * @see AudioDeviceVolumeManager#isInputGainFixed(AudioDeviceAttributes) + */ + @Override + @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED) + public boolean isInputGainFixed(@NonNull AudioDeviceAttributes ada) { + super.isInputGainFixed_enforcePermission(); + + return mInputDeviceVolumeHelper.isInputGainFixed(ada); + } + /** @see AudioManager#setMicrophoneMute(boolean) */ @Override public void setMicrophoneMute(boolean on, String callingPackage, int userId, @@ -10077,6 +10190,14 @@ public class AudioService extends IAudioService.Stub vgs.persistVolumeGroup(msg.arg1); break; + case MSG_SET_INPUT_GAIN_INDEX: + setInputGainIndexInt((AudioDeviceAttributes) msg.obj, msg.arg1); + break; + + case MSG_PERSIST_INPUT_GAIN_INDEX: + persistInputGainIndex((AudioDeviceAttributes) msg.obj, msg.arg1); + break; + case MSG_PERSIST_RINGER_MODE: // note that the value persisted is the current ringer mode, not the // value of ringer mode as of the time the request was made to persist @@ -10147,6 +10268,11 @@ public class AudioService extends IAudioService.Stub mAudioEventWakeLock.release(); break; + case MSG_INIT_INPUT_GAINS: + onInitInputGains(); + mAudioEventWakeLock.release(); + break; + case MSG_INIT_ADI_DEVICE_STATES: onInitAdiDeviceStates(); mAudioEventWakeLock.release(); diff --git a/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java b/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java new file mode 100644 index 000000000000..d83dca629d74 --- /dev/null +++ b/services/core/java/com/android/server/audio/InputDeviceVolumeHelper.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.audio; + +import static android.media.AudioManager.GET_DEVICES_INPUTS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentResolver; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.AudioSystem; +import android.os.UserHandle; +import android.util.IntArray; +import android.util.SparseIntArray; + +import java.util.HashSet; +import java.util.Set; + +/** Maintains the current state of input gains. */ +/*package*/ class InputDeviceVolumeHelper { + private static final String TAG = "InputDeviceVolumeHelper"; + + // TODO(b/364923030): retrieve these constants from AudioPolicyManager. + private final int INDEX_MIN = 0; + private final int INDEX_MAX = 100; + private final int INDEX_DEFAULT = 50; + + private final SettingsAdapter mSettings; + private final ContentResolver mContentResolver; + private final Object mSettingsLock; + private final String mInputGainIndexSettingsName; + + // A map between device internal type (e.g. AudioSystem.DEVICE_IN_BUILTIN_MIC) to its input gain + // index. + private final SparseIntArray mInputGainIndexMap; + private final Set<Integer> mSupportedDeviceTypes; + + InputDeviceVolumeHelper( + SettingsAdapter settings, + ContentResolver contentResolver, + Object settingsLock, + String settingsName) { + mSettings = settings; + mContentResolver = contentResolver; + mSettingsLock = settingsLock; + mInputGainIndexSettingsName = settingsName; + + IntArray internalDeviceTypes = new IntArray(); + int status = AudioSystem.getSupportedDeviceTypes(GET_DEVICES_INPUTS, internalDeviceTypes); + mInputGainIndexMap = + new SparseIntArray( + status == AudioManager.SUCCESS + ? internalDeviceTypes.size() + : AudioSystem.DEVICE_IN_ALL_SET.size()); + + if (status == AudioManager.SUCCESS) { + Set<Integer> supportedDeviceTypes = new HashSet<>(); + for (int i = 0; i < internalDeviceTypes.size(); i++) { + supportedDeviceTypes.add(internalDeviceTypes.get(i)); + } + mSupportedDeviceTypes = supportedDeviceTypes; + } else { + mSupportedDeviceTypes = AudioSystem.DEVICE_IN_ALL_SET; + } + + readSettings(); + } + + public void readSettings() { + synchronized (InputDeviceVolumeHelper.class) { + for (int inputDeviceType : mSupportedDeviceTypes) { + // Retrieve current input gain for device. If no input gain stored for current + // device, use default input gain. + int index; + if (!hasValidSettingsName()) { + index = INDEX_DEFAULT; + } else { + String name = getSettingNameForDevice(inputDeviceType); + index = + mSettings.getSystemIntForUser( + mContentResolver, name, INDEX_DEFAULT, UserHandle.USER_CURRENT); + } + + mInputGainIndexMap.put(inputDeviceType, getValidIndex(index)); + } + } + } + + public boolean hasValidSettingsName() { + return mInputGainIndexSettingsName != null && !mInputGainIndexSettingsName.isEmpty(); + } + + public @Nullable String getSettingNameForDevice(int inputDeviceType) { + if (!hasValidSettingsName()) { + return null; + } + final String suffix = AudioSystem.getInputDeviceName(inputDeviceType); + if (suffix.isEmpty()) { + return mInputGainIndexSettingsName; + } + return mInputGainIndexSettingsName + "_" + suffix; + } + + private int getValidIndex(int index) { + if (index < INDEX_MIN) { + return INDEX_MIN; + } + if (index > INDEX_MAX) { + return INDEX_MAX; + } + return index; + } + + public int getInputGainIndex(@NonNull AudioDeviceAttributes ada) { + int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType()); + ensureValidInputDeviceType(inputDeviceType); + + synchronized (InputDeviceVolumeHelper.class) { + return mInputGainIndexMap.get(inputDeviceType, INDEX_DEFAULT); + } + } + + public int getMaxInputGainIndex() { + return INDEX_MAX; + } + + public int getMinInputGainIndex() { + return INDEX_MIN; + } + + public boolean isInputGainFixed(@NonNull AudioDeviceAttributes ada) { + int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType()); + ensureValidInputDeviceType(inputDeviceType); + + // For simplicity, all devices have non fixed input gain. This might change + // when more input devices are supported and some do not support input gain control. + return false; + } + + public boolean setInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) { + int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType()); + ensureValidInputDeviceType(inputDeviceType); + + int oldIndex; + synchronized (mSettingsLock) { + synchronized (InputDeviceVolumeHelper.class) { + oldIndex = getInputGainIndex(ada); + index = getValidIndex(index); + + if (oldIndex == index) { + return false; + } + + mInputGainIndexMap.put(inputDeviceType, index); + return true; + } + } + } + + public void persistInputGainIndex(@NonNull AudioDeviceAttributes ada, int index) { + int inputDeviceType = AudioDeviceInfo.convertDeviceTypeToInternalInputDevice(ada.getType()); + ensureValidInputDeviceType(inputDeviceType); + + if (hasValidSettingsName()) { + mSettings.putSystemIntForUser( + mContentResolver, + getSettingNameForDevice(inputDeviceType), + index, + UserHandle.USER_CURRENT); + } + } + + public boolean isValidInputDeviceType(int inputDeviceType) { + return mSupportedDeviceTypes.contains(inputDeviceType); + } + + private void ensureValidInputDeviceType(int inputDeviceType) { + if (!isValidInputDeviceType(inputDeviceType)) { + throw new IllegalArgumentException("Bad input device type " + inputDeviceType); + } + } +} diff --git a/services/core/java/com/android/server/incident/PendingReports.java b/services/core/java/com/android/server/incident/PendingReports.java index adcda0a63152..35b3673fdf77 100644 --- a/services/core/java/com/android/server/incident/PendingReports.java +++ b/services/core/java/com/android/server/incident/PendingReports.java @@ -304,16 +304,16 @@ class PendingReports { denyReportBeforeAddingRec(listener, callingPackage); return; } + AttributionSource attributionSource = + new AttributionSource.Builder(callingUid) + .setPackageName(callingPackage) + .build(); // Only with userdebug/eng build: it could check capture consentless bugreport permission // and approve the report when it's granted. boolean captureConsentlessBugreportOnUserdebugBuildGranted = false; if ((Build.IS_USERDEBUG || Build.IS_ENG) && (flags & IncidentManager.FLAG_ALLOW_CONSENTLESS_BUGREPORT) != 0) { - AttributionSource attributionSource = - new AttributionSource.Builder(callingUid) - .setPackageName(callingPackage) - .build(); captureConsentlessBugreportOnUserdebugBuildGranted = mPermissionManager.checkPermissionForDataDelivery( Manifest.permission.CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD, @@ -321,12 +321,32 @@ class PendingReports { /* message= */ null) == PERMISSION_GRANTED; } - if (captureConsentlessBugreportOnUserdebugBuildGranted) { + + // Allow system apps to skip the consent dialog and use their in-built consent mechanism + // instead. + boolean captureConsentlessBugreportDelegatedConsentGranted = false; + if ((flags & IncidentManager.FLAG_ALLOW_CONSENTLESS_BUGREPORT) != 0) { + captureConsentlessBugreportDelegatedConsentGranted = + mPermissionManager.checkPermissionForDataDelivery( + Manifest.permission + .CAPTURE_CONSENTLESS_BUGREPORT_DELEGATED_CONSENT, + attributionSource, + /* message= */ null) + == PERMISSION_GRANTED; + } + + if (captureConsentlessBugreportOnUserdebugBuildGranted + || captureConsentlessBugreportDelegatedConsentGranted) { try { PendingReportRec rec = new PendingReportRec( callingPackage, receiverClass, reportId, flags, listener); - Log.d(TAG, "approving consentless report: " + rec.getUri()); + if (captureConsentlessBugreportOnUserdebugBuildGranted) { + Log.d(TAG, "approving consentless report: " + rec.getUri()); + } + if (captureConsentlessBugreportDelegatedConsentGranted) { + Log.d(TAG, "delegating consent for report: " + rec.getUri()); + } listener.onReportApproved(); return; } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/incident/TEST_MAPPING b/services/core/java/com/android/server/incident/TEST_MAPPING new file mode 100644 index 000000000000..4f789dbba2b8 --- /dev/null +++ b/services/core/java/com/android/server/incident/TEST_MAPPING @@ -0,0 +1,10 @@ +{ + "postsubmit": [ + { + "name": "CtsRootBugreportTestCases" + }, + { + "name": "BugreportManagerTestCases" + } + ] +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index c888eef7f5df..e40d855293cd 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -270,4 +270,18 @@ public abstract class InputManagerInternal { * @param scaleFactor the new scale factor to be applied for pointer icons. */ public abstract void setAccessibilityPointerIconScaleFactor(int displayId, float scaleFactor); + + /** + * Set whether the given input device can wake up the kernel from sleep + * when it generates input events. By default, usually only internal (built-in) + * input devices can wake the kernel from sleep. For an external input device + * that supports remote wakeup to be able to wake the kernel, this must be called + * after each time the device is connected/added. + * + * @param deviceId the device ID of the input device. + * @param enabled When true, device will be configured to wake up kernel. + * + * @return true if setting power wakeup was successful. + */ + public abstract boolean setKernelWakeEnabled(int deviceId, boolean enabled); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 98e5319cde30..bea520f9429e 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -3511,6 +3511,11 @@ public class InputManagerService extends IInputManager.Stub public void setAccessibilityPointerIconScaleFactor(int displayId, float scaleFactor) { InputManagerService.this.setAccessibilityPointerIconScaleFactor(displayId, scaleFactor); } + + @Override + public boolean setKernelWakeEnabled(int deviceId, boolean enabled) { + return mNative.setKernelWakeEnabled(deviceId, enabled); + } } @Override diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 21e8bccd2883..283fdea92b63 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -287,6 +287,17 @@ interface NativeInputManagerService { */ int getLastUsedInputDeviceId(); + /** + * Set whether the given input device can wake up the kernel from sleep + * when it generates input events. By default, usually only internal (built-in) + * input devices can wake the kernel from sleep. For an external input device + * that supports remote wakeup to be able to wake the kernel, this must be called + * after each time the device is connected/added. + * + * Returns true if setting power wakeup was successful. + */ + boolean setKernelWakeEnabled(int deviceId, boolean enabled); + /** The native implementation of InputManagerService methods. */ class NativeImpl implements NativeInputManagerService { /** Pointer to native input manager service object, used by native code. */ @@ -573,5 +584,8 @@ interface NativeInputManagerService { @Override public native int getLastUsedInputDeviceId(); + + @Override + public native boolean setKernelWakeEnabled(int deviceId, boolean enabled); } } diff --git a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java index 24b090c6ffab..734c61b6b2a8 100644 --- a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java +++ b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java @@ -42,6 +42,7 @@ import com.android.server.notification.NotificationManagerService.DumpFilter; import com.android.server.pm.PackageManagerService; import java.io.PrintWriter; +import java.time.Clock; import java.util.ArrayList; import java.util.Calendar; import java.util.List; @@ -62,6 +63,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { private static final String SCP_SETTING = "snoozed_schedule_condition_provider"; private final Context mContext = this; + private final Clock mClock; private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>(); @GuardedBy("mSnoozedForAlarm") private final ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>(); @@ -72,7 +74,13 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { private long mNextAlarmTime; public ScheduleConditionProvider() { + this(Clock.systemUTC()); + } + + @VisibleForTesting + ScheduleConditionProvider(Clock clock) { if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()"); + mClock = clock; } @Override @@ -86,7 +94,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { pw.print(" mConnected="); pw.println(mConnected); pw.print(" mRegistered="); pw.println(mRegistered); pw.println(" mSubscriptions="); - final long now = System.currentTimeMillis(); + final long now = mClock.millis(); synchronized (mSubscriptions) { for (Uri conditionId : mSubscriptions.keySet()) { pw.print(" "); @@ -117,7 +125,9 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { @Override public void onUserSwitched(UserHandle user) { - // Nothing to do because time-based schedules are not tied to any user data. + // Nothing to do here because evaluateSubscriptions() is called for the new configuration + // when users switch, and that will reevaluate the next alarm, which is the only piece that + // is user-dependent. } @Override @@ -151,12 +161,9 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { } private void evaluateSubscriptions() { - if (mAlarmManager == null) { - mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - } - final long now = System.currentTimeMillis(); + final long now = mClock.millis(); mNextAlarmTime = 0; - long nextUserAlarmTime = getNextAlarm(); + long nextUserAlarmTime = getNextAlarmClockAlarm(); List<Condition> conditionsToNotify = new ArrayList<>(); synchronized (mSubscriptions) { setRegistered(!mSubscriptions.isEmpty()); @@ -232,7 +239,10 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } - public long getNextAlarm() { + private long getNextAlarmClockAlarm() { + if (mAlarmManager == null) { + mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + } final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock( ActivityManager.getCurrentUser()); return info != null ? info.getTriggerTime() : 0; @@ -252,8 +262,13 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); filter.addAction(ACTION_EVALUATE); filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); - registerReceiver(mReceiver, filter, - Context.RECEIVER_EXPORTED_UNAUDITED); + if (android.app.Flags.modesHsum()) { + registerReceiverForAllUsers(mReceiver, filter, /* broadcastPermission= */ null, + /* scheduler= */ null); + } else { + registerReceiver(mReceiver, filter, + Context.RECEIVER_EXPORTED_UNAUDITED); + } } else { unregisterReceiver(mReceiver); } @@ -327,10 +342,18 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { } } - private BroadcastReceiver mReceiver = new BroadcastReceiver() { + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction()); + if (android.app.Flags.modesHsum()) { + if (AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equals(intent.getAction()) + && getSendingUserId() != ActivityManager.getCurrentUser()) { + // A different user changed their next alarm. + return; + } + } + if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { synchronized (mSubscriptions) { for (Uri conditionId : mSubscriptions.keySet()) { @@ -345,4 +368,8 @@ public class ScheduleConditionProvider extends SystemConditionProviderService { } }; + @VisibleForTesting // otherwise = NONE + public ArrayMap<Uri, ScheduleCalendar> getSubscriptions() { + return mSubscriptions; + } } diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java index dc6b1644db4d..78bc06c27130 100644 --- a/services/core/java/com/android/server/power/ThermalManagerService.java +++ b/services/core/java/com/android/server/power/ThermalManagerService.java @@ -31,6 +31,7 @@ import android.content.Context; import android.hardware.thermal.IThermal; import android.hardware.thermal.IThermalChangedCallback; import android.hardware.thermal.TemperatureThreshold; +import android.hardware.thermal.TemperatureType; import android.hardware.thermal.ThrottlingSeverity; import android.hardware.thermal.V1_0.ThermalStatus; import android.hardware.thermal.V1_0.ThermalStatusCode; @@ -134,6 +135,31 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting final TemperatureWatcher mTemperatureWatcher = new TemperatureWatcher(); + private final ThermalHalWrapper.WrapperThermalChangedCallback mWrapperCallback = + new ThermalHalWrapper.WrapperThermalChangedCallback() { + @Override + public void onTemperatureChanged(Temperature temperature) { + final long token = Binder.clearCallingIdentity(); + try { + ThermalManagerService.this.onTemperatureChanged(temperature, true); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onThresholdChanged(TemperatureThreshold threshold) { + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mTemperatureWatcher.mSamples) { + mTemperatureWatcher.updateTemperatureThresholdLocked(threshold, true); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + }; + private final Context mContext; public ThermalManagerService(Context context) { @@ -146,7 +172,7 @@ public class ThermalManagerService extends SystemService { mContext = context; mHalWrapper = halWrapper; if (halWrapper != null) { - halWrapper.setCallback(this::onTemperatureChangedCallback); + halWrapper.setCallback(mWrapperCallback); } mStatus = Temperature.THROTTLING_NONE; } @@ -171,19 +197,19 @@ public class ThermalManagerService extends SystemService { // Connect to HAL and post to listeners. boolean halConnected = (mHalWrapper != null); if (!halConnected) { - mHalWrapper = new ThermalHalAidlWrapper(this::onTemperatureChangedCallback); + mHalWrapper = new ThermalHalAidlWrapper(mWrapperCallback); halConnected = mHalWrapper.connectToHal(); } if (!halConnected) { - mHalWrapper = new ThermalHal20Wrapper(this::onTemperatureChangedCallback); + mHalWrapper = new ThermalHal20Wrapper(mWrapperCallback); halConnected = mHalWrapper.connectToHal(); } if (!halConnected) { - mHalWrapper = new ThermalHal11Wrapper(this::onTemperatureChangedCallback); + mHalWrapper = new ThermalHal11Wrapper(mWrapperCallback); halConnected = mHalWrapper.connectToHal(); } if (!halConnected) { - mHalWrapper = new ThermalHal10Wrapper(this::onTemperatureChangedCallback); + mHalWrapper = new ThermalHal10Wrapper(mWrapperCallback); halConnected = mHalWrapper.connectToHal(); } if (!halConnected) { @@ -200,7 +226,7 @@ public class ThermalManagerService extends SystemService { onTemperatureChanged(temperatures.get(i), false); } onTemperatureMapChangedLocked(); - mTemperatureWatcher.updateThresholds(); + mTemperatureWatcher.getAndUpdateThresholds(); mHalReady.set(true); } } @@ -335,16 +361,6 @@ public class ThermalManagerService extends SystemService { } } - /* HwBinder callback **/ - private void onTemperatureChangedCallback(Temperature temperature) { - final long token = Binder.clearCallingIdentity(); - try { - onTemperatureChanged(temperature, true); - } finally { - Binder.restoreCallingIdentity(token); - } - } - private void registerStatsCallbacks() { final StatsManager statsManager = mContext.getSystemService(StatsManager.class); if (statsManager != null) { @@ -924,19 +940,19 @@ public class ThermalManagerService extends SystemService { /** Lock to protect HAL handle. */ protected final Object mHalLock = new Object(); - @FunctionalInterface - interface TemperatureChangedCallback { - void onValues(Temperature temperature); + interface WrapperThermalChangedCallback { + void onTemperatureChanged(Temperature temperature); + void onThresholdChanged(TemperatureThreshold threshold); } /** Temperature callback. */ - protected TemperatureChangedCallback mCallback; + protected WrapperThermalChangedCallback mCallback; /** Cookie for matching the right end point. */ protected static final int THERMAL_HAL_DEATH_COOKIE = 5612; @VisibleForTesting - protected void setCallback(TemperatureChangedCallback cb) { + protected void setCallback(WrapperThermalChangedCallback cb) { mCallback = cb; } @@ -959,7 +975,7 @@ public class ThermalManagerService extends SystemService { List<Temperature> temperatures = getCurrentTemperatures(false, 0); final int count = temperatures.size(); for (int i = 0; i < count; i++) { - mCallback.onValues(temperatures.get(i)); + mCallback.onTemperatureChanged(temperatures.get(i)); } } } @@ -985,31 +1001,42 @@ public class ThermalManagerService extends SystemService { private IThermal mInstance = null; /** Callback for Thermal HAL AIDL. */ - private final IThermalChangedCallback mThermalChangedCallback = + private final IThermalChangedCallback mThermalCallbackAidl = new IThermalChangedCallback.Stub() { - @Override public void notifyThrottling( - android.hardware.thermal.Temperature temperature) - throws RemoteException { + @Override + public void notifyThrottling( + android.hardware.thermal.Temperature temperature) { Temperature svcTemperature = new Temperature(temperature.value, temperature.type, temperature.name, temperature.throttlingStatus); final long token = Binder.clearCallingIdentity(); try { - mCallback.onValues(svcTemperature); + mCallback.onTemperatureChanged(svcTemperature); } finally { Binder.restoreCallingIdentity(token); } } - @Override public int getInterfaceVersion() throws RemoteException { - return this.VERSION; - } + @Override + public void notifyThresholdChanged(TemperatureThreshold threshold) { + if (Flags.allowThermalThresholdsCallback()) { + if (threshold.type == TemperatureType.SKIN) { + mCallback.onThresholdChanged(threshold); + } + } + } - @Override public String getInterfaceHash() throws RemoteException { - return this.HASH; - } - }; + @Override + public int getInterfaceVersion() throws RemoteException { + return this.VERSION; + } - ThermalHalAidlWrapper(TemperatureChangedCallback callback) { + @Override + public String getInterfaceHash() throws RemoteException { + return this.HASH; + } + }; + + ThermalHalAidlWrapper(WrapperThermalChangedCallback callback) { mCallback = callback; } @@ -1153,7 +1180,7 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting void registerThermalChangedCallback() { try { - mInstance.registerThermalChangedCallback(mThermalChangedCallback); + mInstance.registerThermalChangedCallback(mThermalCallbackAidl); } catch (IllegalArgumentException | IllegalStateException e) { Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", e); @@ -1185,7 +1212,7 @@ public class ThermalManagerService extends SystemService { @GuardedBy("mHalLock") private android.hardware.thermal.V1_0.IThermal mThermalHal10 = null; - ThermalHal10Wrapper(TemperatureChangedCallback callback) { + ThermalHal10Wrapper(WrapperThermalChangedCallback callback) { mCallback = callback; } @@ -1317,14 +1344,14 @@ public class ThermalManagerService extends SystemService { : Temperature.THROTTLING_NONE); final long token = Binder.clearCallingIdentity(); try { - mCallback.onValues(thermalSvcTemp); + mCallback.onTemperatureChanged(thermalSvcTemp); } finally { Binder.restoreCallingIdentity(token); } } }; - ThermalHal11Wrapper(TemperatureChangedCallback callback) { + ThermalHal11Wrapper(WrapperThermalChangedCallback callback) { mCallback = callback; } @@ -1455,14 +1482,14 @@ public class ThermalManagerService extends SystemService { temperature.throttlingStatus); final long token = Binder.clearCallingIdentity(); try { - mCallback.onValues(thermalSvcTemp); + mCallback.onTemperatureChanged(thermalSvcTemp); } finally { Binder.restoreCallingIdentity(token); } } }; - ThermalHal20Wrapper(TemperatureChangedCallback callback) { + ThermalHal20Wrapper(WrapperThermalChangedCallback callback) { mCallback = callback; } @@ -1627,52 +1654,57 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting long mInactivityThresholdMillis = INACTIVITY_THRESHOLD_MILLIS; - void updateThresholds() { + void getAndUpdateThresholds() { List<TemperatureThreshold> thresholds = mHalWrapper.getTemperatureThresholds(true, Temperature.TYPE_SKIN); synchronized (mSamples) { if (Flags.allowThermalHeadroomThresholds()) { Arrays.fill(mHeadroomThresholds, Float.NaN); } - for (int t = 0; t < thresholds.size(); ++t) { - TemperatureThreshold threshold = thresholds.get(t); - if (threshold.hotThrottlingThresholds.length <= ThrottlingSeverity.SEVERE) { - continue; - } - float severeThreshold = - threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE]; - if (!Float.isNaN(severeThreshold)) { - mSevereThresholds.put(threshold.name, severeThreshold); - if (Flags.allowThermalHeadroomThresholds()) { - for (int severity = ThrottlingSeverity.LIGHT; - severity <= ThrottlingSeverity.SHUTDOWN; severity++) { - if (threshold.hotThrottlingThresholds.length > severity) { - updateHeadroomThreshold(severity, - threshold.hotThrottlingThresholds[severity], - severeThreshold); - } - } - } - } + for (final TemperatureThreshold threshold : thresholds) { + updateTemperatureThresholdLocked(threshold, false); } } } // For an older device with multiple SKIN sensors, we will set a severity's headroom - // threshold based on the minimum value of all as a workaround. - void updateHeadroomThreshold(int severity, float threshold, float severeThreshold) { - if (!Float.isNaN(threshold)) { - synchronized (mSamples) { - if (severity == ThrottlingSeverity.SEVERE) { - mHeadroomThresholds[severity] = 1.0f; - return; + // threshold based on the minimum value of all as a workaround, unless override. + @GuardedBy("mSamples") + void updateTemperatureThresholdLocked(TemperatureThreshold threshold, boolean override) { + if (threshold.hotThrottlingThresholds.length <= ThrottlingSeverity.SEVERE) { + return; + } + float severeThreshold = + threshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE]; + if (Float.isNaN(severeThreshold)) { + return; + } + mSevereThresholds.put(threshold.name, severeThreshold); + if (!Flags.allowThermalHeadroomThresholds()) { + return; + } + if (override) { + Arrays.fill(mHeadroomThresholds, Float.NaN); + } + for (int severity = ThrottlingSeverity.LIGHT; + severity <= ThrottlingSeverity.SHUTDOWN; severity++) { + if (threshold.hotThrottlingThresholds.length > severity) { + float t = threshold.hotThrottlingThresholds[severity]; + if (Float.isNaN(t)) { + continue; } - float headroom = normalizeTemperature(threshold, severeThreshold); - if (Float.isNaN(mHeadroomThresholds[severity])) { - mHeadroomThresholds[severity] = headroom; - } else { - float lastHeadroom = mHeadroomThresholds[severity]; - mHeadroomThresholds[severity] = Math.min(lastHeadroom, headroom); + synchronized (mSamples) { + if (severity == ThrottlingSeverity.SEVERE) { + mHeadroomThresholds[severity] = 1.0f; + continue; + } + float headroom = normalizeTemperature(t, severeThreshold); + if (Float.isNaN(mHeadroomThresholds[severity])) { + mHeadroomThresholds[severity] = headroom; + } else { + float lastHeadroom = mHeadroomThresholds[severity]; + mHeadroomThresholds[severity] = Math.min(lastHeadroom, headroom); + } } } } diff --git a/services/core/java/com/android/server/power/WakefulnessSessionObserver.java b/services/core/java/com/android/server/power/WakefulnessSessionObserver.java index c6b260288a88..64f0693f14c4 100644 --- a/services/core/java/com/android/server/power/WakefulnessSessionObserver.java +++ b/services/core/java/com/android/server/power/WakefulnessSessionObserver.java @@ -49,6 +49,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.util.IndentingPrintWriter; +import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.DisplayAddress; @@ -70,12 +71,11 @@ import java.lang.annotation.RetentionPolicy; public class WakefulnessSessionObserver { private static final String TAG = "WakefulnessSessionObserver"; - private static final int OFF_REASON_UNKNOWN = FrameworkStatsLog + static final int OFF_REASON_UNKNOWN = FrameworkStatsLog .SCREEN_INTERACTIVE_SESSION_REPORTED__INTERACTIVE_STATE_OFF_REASON__UNKNOWN; - private static final int OFF_REASON_TIMEOUT = FrameworkStatsLog + static final int OFF_REASON_TIMEOUT = FrameworkStatsLog .SCREEN_INTERACTIVE_SESSION_REPORTED__INTERACTIVE_STATE_OFF_REASON__TIMEOUT; - @VisibleForTesting - protected static final int OFF_REASON_POWER_BUTTON = FrameworkStatsLog + static final int OFF_REASON_POWER_BUTTON = FrameworkStatsLog .SCREEN_INTERACTIVE_SESSION_REPORTED__INTERACTIVE_STATE_OFF_REASON__POWER_BUTTON; /** @@ -90,25 +90,21 @@ public class WakefulnessSessionObserver { @Retention(RetentionPolicy.SOURCE) private @interface OffReason {} - private static final int OVERRIDE_OUTCOME_UNKNOWN = FrameworkStatsLog + static final int OVERRIDE_OUTCOME_UNKNOWN = FrameworkStatsLog .SCREEN_TIMEOUT_OVERRIDE_REPORTED__OVERRIDE_OUTCOME__UNKNOWN; - @VisibleForTesting - protected static final int OVERRIDE_OUTCOME_TIMEOUT_SUCCESS = FrameworkStatsLog + static final int OVERRIDE_OUTCOME_TIMEOUT_SUCCESS = FrameworkStatsLog .SCREEN_TIMEOUT_OVERRIDE_REPORTED__OVERRIDE_OUTCOME__TIMEOUT_SUCCESS; - @VisibleForTesting - protected static final int OVERRIDE_OUTCOME_TIMEOUT_USER_INITIATED_REVERT = FrameworkStatsLog + static final int OVERRIDE_OUTCOME_TIMEOUT_USER_INITIATED_REVERT = FrameworkStatsLog .SCREEN_TIMEOUT_OVERRIDE_REPORTED__OVERRIDE_OUTCOME__TIMEOUT_USER_INITIATED_REVERT; - private static final int OVERRIDE_OUTCOME_CANCEL_CLIENT_API_CALL = FrameworkStatsLog + static final int OVERRIDE_OUTCOME_CANCEL_CLIENT_API_CALL = FrameworkStatsLog .SCREEN_TIMEOUT_OVERRIDE_REPORTED__OVERRIDE_OUTCOME__CANCEL_CLIENT_API_CALL; - @VisibleForTesting - protected static final int OVERRIDE_OUTCOME_CANCEL_USER_INTERACTION = FrameworkStatsLog + static final int OVERRIDE_OUTCOME_CANCEL_USER_INTERACTION = FrameworkStatsLog .SCREEN_TIMEOUT_OVERRIDE_REPORTED__OVERRIDE_OUTCOME__CANCEL_USER_INTERACTION; - @VisibleForTesting - protected static final int OVERRIDE_OUTCOME_CANCEL_POWER_BUTTON = FrameworkStatsLog + static final int OVERRIDE_OUTCOME_CANCEL_POWER_BUTTON = FrameworkStatsLog .SCREEN_TIMEOUT_OVERRIDE_REPORTED__OVERRIDE_OUTCOME__CANCEL_POWER_BUTTON; - private static final int OVERRIDE_OUTCOME_CANCEL_CLIENT_DISCONNECT = FrameworkStatsLog + static final int OVERRIDE_OUTCOME_CANCEL_CLIENT_DISCONNECT = FrameworkStatsLog .SCREEN_TIMEOUT_OVERRIDE_REPORTED__OVERRIDE_OUTCOME__CANCEL_CLIENT_DISCONNECTED; - private static final int OVERRIDE_OUTCOME_CANCEL_OTHER = FrameworkStatsLog + static final int OVERRIDE_OUTCOME_CANCEL_OTHER = FrameworkStatsLog .SCREEN_TIMEOUT_OVERRIDE_REPORTED__OVERRIDE_OUTCOME__CANCEL_OTHER; /** @@ -128,19 +124,15 @@ public class WakefulnessSessionObserver { @Retention(RetentionPolicy.SOURCE) private @interface OverrideOutcome {} - private static final int POLICY_REASON_UNKNOWN = FrameworkStatsLog + static final int POLICY_REASON_UNKNOWN = FrameworkStatsLog .SCREEN_DIM_REPORTED__POLICY_REASON__UNKNOWN; - @VisibleForTesting - protected static final int POLICY_REASON_OFF_TIMEOUT = FrameworkStatsLog + static final int POLICY_REASON_OFF_TIMEOUT = FrameworkStatsLog .SCREEN_DIM_REPORTED__POLICY_REASON__OFF_TIMEOUT; - @VisibleForTesting - protected static final int POLICY_REASON_OFF_POWER_BUTTON = FrameworkStatsLog + static final int POLICY_REASON_OFF_POWER_BUTTON = FrameworkStatsLog .SCREEN_DIM_REPORTED__POLICY_REASON__OFF_POWER_BUTTON; - @VisibleForTesting - protected static final int POLICY_REASON_BRIGHT_UNDIM = FrameworkStatsLog + static final int POLICY_REASON_BRIGHT_UNDIM = FrameworkStatsLog .SCREEN_DIM_REPORTED__POLICY_REASON__BRIGHT_UNDIM; - @VisibleForTesting - protected static final int POLICY_REASON_BRIGHT_INITIATED_REVERT = FrameworkStatsLog + static final int POLICY_REASON_BRIGHT_INITIATED_REVERT = FrameworkStatsLog .SCREEN_DIM_REPORTED__POLICY_REASON__BRIGHT_INITIATED_REVERT; /** @@ -157,21 +149,18 @@ public class WakefulnessSessionObserver { @Retention(RetentionPolicy.SOURCE) private @interface PolicyReason {} - @VisibleForTesting protected static final int DEFAULT_USER_ACTIVITY = USER_ACTIVITY_EVENT_OTHER; - private static final long USER_INITIATED_REVERT_THRESHOLD_MILLIS = 5000L; - private static final long SEND_OVERRIDE_TIMEOUT_LOG_THRESHOLD_MILLIS = 1000L; - @VisibleForTesting - protected static final long SCREEN_POLICY_DIM_POWER_OFF_BRIGHT_THRESHOLD_MILLIS = 500L; + static final int DEFAULT_USER_ACTIVITY = USER_ACTIVITY_EVENT_OTHER; + static final long USER_INITIATED_REVERT_THRESHOLD_MILLIS = 5000L; + static final long SEND_OVERRIDE_TIMEOUT_LOG_THRESHOLD_MILLIS = 1000L; + static final long SCREEN_POLICY_DIM_POWER_OFF_BRIGHT_THRESHOLD_MILLIS = 500L; - @VisibleForTesting protected static final Object HANDLER_TOKEN = new Object(); + static final Object HANDLER_TOKEN = new Object(); private Context mContext; private int mScreenOffTimeoutMs; private int mOverrideTimeoutMs = 0; - @VisibleForTesting - protected final SparseArray<WakefulnessSessionPowerGroup> mPowerGroups = new SparseArray<>(); - @VisibleForTesting - protected WakefulnessSessionFrameworkStatsLogger mWakefulnessSessionFrameworkStatsLogger; + final SparseArray<WakefulnessSessionPowerGroup> mPowerGroups = new SparseArray<>(); + WakefulnessSessionFrameworkStatsLogger mWakefulnessSessionFrameworkStatsLogger; private final Clock mClock; private final Object mLock = new Object(); private final Handler mHandler; @@ -347,7 +336,8 @@ public class WakefulnessSessionObserver { writer.println(); } - private void updateSettingScreenOffTimeout(Context context) { + @VisibleForTesting + void updateSettingScreenOffTimeout(Context context) { synchronized (mLock) { mScreenOffTimeoutMs = Settings.System.getIntForUser( context.getContentResolver(), @@ -453,6 +443,7 @@ public class WakefulnessSessionObserver { return; } + final int screenOffTimeoutMs = getScreenOffTimeout(); mIsInteractive = isInteractive(wakefulness); if (mIsInteractive) { mInteractiveStateOnStartTimestamp = eventTime; @@ -466,7 +457,7 @@ public class WakefulnessSessionObserver { mPowerGroupId, OVERRIDE_OUTCOME_TIMEOUT_USER_INITIATED_REVERT, mOverrideTimeoutMs, - getScreenOffTimeout()); + screenOffTimeoutMs); mSendOverrideTimeoutLogTimestamp = eventTime; } mTimeoutOffTimestamp = TIMEOUT_OFF_RESET_TIMESTAMP; @@ -496,7 +487,7 @@ public class WakefulnessSessionObserver { mPowerGroupId, OVERRIDE_OUTCOME_CANCEL_POWER_BUTTON, mOverrideTimeoutMs, - getScreenOffTimeout()); + screenOffTimeoutMs); mSendOverrideTimeoutLogTimestamp = eventTime; mTimeoutOverrideReleaseReason = RELEASE_REASON_UNKNOWN; // reset the reason } @@ -514,13 +505,12 @@ public class WakefulnessSessionObserver { // timeout has been done successfully. if (isInOverrideTimeout()) { reducedInteractiveStateOnDurationMs = - getScreenOffTimeout() - mOverrideTimeoutMs; - + screenOffTimeoutMs - mOverrideTimeoutMs; mWakefulnessSessionFrameworkStatsLogger.logTimeoutOverrideEvent( mPowerGroupId, OVERRIDE_OUTCOME_TIMEOUT_SUCCESS, mOverrideTimeoutMs, - getScreenOffTimeout()); + screenOffTimeoutMs); mSendOverrideTimeoutLogTimestamp = eventTime; // Record a timestamp to track if the user initiates to revert from off @@ -533,13 +523,21 @@ public class WakefulnessSessionObserver { long interactiveStateOnDurationMs = eventTime - mInteractiveStateOnStartTimestamp; - mWakefulnessSessionFrameworkStatsLogger.logSessionEvent( - mPowerGroupId, - interactiveStateOffReason, - interactiveStateOnDurationMs, - lastUserActivity, - lastUserActivityDurationMs, - reducedInteractiveStateOnDurationMs); + + if (reducedInteractiveStateOnDurationMs < screenOffTimeoutMs + && reducedInteractiveStateOnDurationMs >= 0) { + mWakefulnessSessionFrameworkStatsLogger.logSessionEvent( + mPowerGroupId, + interactiveStateOffReason, + interactiveStateOnDurationMs, + lastUserActivity, + lastUserActivityDurationMs, + reducedInteractiveStateOnDurationMs); + } else { + Slog.w(TAG, "invalid reducedInteractiveStateOnDurationMs: " + + reducedInteractiveStateOnDurationMs); + } + } } @@ -608,6 +606,7 @@ public class WakefulnessSessionObserver { return; } + final int screenOffTimeoutMs = getScreenOffTimeout(); int dimDurationMs = 0; int lastUserActivity = mCurrentUserActivityEvent; int lastUserActivityDurationMs = (int) (eventTime - mCurrentUserActivityTimestamp); @@ -625,7 +624,7 @@ public class WakefulnessSessionObserver { lastUserActivity, lastUserActivityDurationMs, dimDurationMs, - mScreenOffTimeoutMs); + screenOffTimeoutMs); mPastDimDurationMs = dimDurationMs; return; } @@ -645,7 +644,7 @@ public class WakefulnessSessionObserver { lastUserActivity, lastUserActivityDurationMs, dimDurationMs, - mScreenOffTimeoutMs); + screenOffTimeoutMs); mHandler.removeCallbacksAndMessages(HANDLER_TOKEN); } @@ -674,7 +673,7 @@ public class WakefulnessSessionObserver { savedLastUserActivity, savedLastUserActivityDurationMs, savedDimDurationMs, - mScreenOffTimeoutMs); + screenOffTimeoutMs); mPastDimDurationMs = savedDimDurationMs; }, HANDLER_TOKEN, SCREEN_POLICY_DIM_POWER_OFF_BRIGHT_THRESHOLD_MILLIS); } @@ -692,7 +691,7 @@ public class WakefulnessSessionObserver { lastUserActivity, lastUserActivityDurationMs, mPastDimDurationMs, - mScreenOffTimeoutMs); + screenOffTimeoutMs); } return; } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 2c4179fb6d88..0d987074de76 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1757,6 +1757,13 @@ class ActivityStarter { startedActivityRootTask.setAlwaysOnTop(true); } + if (isIndependentLaunch && !mDoResume && avoidMoveToFront() && !mTransientLaunch + && !started.shouldBeVisible(true /* ignoringKeyguard */)) { + Slog.i(TAG, "Abort " + transition + " of invisible launch " + started); + transition.abort(); + return startedActivityRootTask; + } + // If there is no state change (e.g. a resumed activity is reparented to top of // another display) to trigger a visibility/configuration checking, we have to // update the configuration for changing to different display. diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 04a625b4a237..5b2fecd584b0 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -65,10 +65,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerPolicyConstants.ACTION_HDMI_PLUGGED; import static android.view.WindowManagerPolicyConstants.EXTRA_HDMI_PLUGGED_STATE; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_INVALID; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_RIGHT; import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM; @@ -109,7 +105,6 @@ import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import android.view.DisplayInfo; -import android.view.Gravity; import android.view.InsetsFlags; import android.view.InsetsFrameProvider; import android.view.InsetsSource; @@ -142,7 +137,6 @@ import com.android.internal.view.AppearanceRegion; import com.android.internal.widget.PointerLocationView; import com.android.server.LocalServices; import com.android.server.UiThread; -import com.android.server.policy.WindowManagerPolicy.NavigationBarPosition; import com.android.server.policy.WindowManagerPolicy.ScreenOnListener; import com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs; import com.android.server.statusbar.StatusBarManagerInternal; @@ -263,8 +257,7 @@ public class DisplayPolicy { private WindowState mStatusBar = null; private volatile WindowState mNotificationShade; private WindowState mNavigationBar = null; - @NavigationBarPosition - private int mNavigationBarPosition = NAV_BAR_BOTTOM; + private boolean mHasBottomNavigationBar = true; private final ArraySet<WindowState> mInsetsSourceWindowsExceptIme = new ArraySet<>(); @@ -1255,8 +1248,7 @@ public class DisplayPolicy { throw new IllegalArgumentException("IME insets must be provided by a window."); } - if (!ENABLE_HIDE_IME_CAPTION_BAR && mNavigationBar != null - && navigationBarPosition(displayFrames.mRotation) == NAV_BAR_BOTTOM) { + if (!ENABLE_HIDE_IME_CAPTION_BAR && mNavigationBar != null && mHasBottomNavigationBar) { // In gesture navigation, nav bar frame is larger than frame to calculate insets. // IME should not provide frame which is smaller than the nav bar frame. Otherwise, // nav bar might be overlapped with the content of the client when IME is shown. @@ -1469,10 +1461,9 @@ public class DisplayPolicy { public void applyPostLayoutPolicyLw(WindowState win, WindowManager.LayoutParams attrs, WindowState attached, WindowState imeTarget) { if (attrs.type == TYPE_NAVIGATION_BAR) { - // Keep mNavigationBarPosition updated to make sure the transient detection and bar - // color control is working correctly. - final DisplayFrames displayFrames = mDisplayContent.mDisplayFrames; - mNavigationBarPosition = navigationBarPosition(displayFrames.mRotation); + // Keep mHasBottomNavigationBar updated to make sure the bar color control is working + // correctly. + mHasBottomNavigationBar = hasBottomNavigationBar(); } final boolean affectsSystemUi = win.canAffectSystemUiFlags(); if (DEBUG_LAYOUT) Slog.i(TAG, "Win " + win + ": affectsSystemUi=" + affectsSystemUi); @@ -2230,20 +2221,11 @@ public class DisplayPolicy { mDisplayContent.mDisplayUpdater.onDisplaySwitching(true); } - @NavigationBarPosition - int navigationBarPosition(int displayRotation) { - if (mNavigationBar != null) { - final int gravity = mNavigationBar.mAttrs.forRotation(displayRotation).gravity; - switch (gravity) { - case Gravity.LEFT: - return NAV_BAR_LEFT; - case Gravity.RIGHT: - return NAV_BAR_RIGHT; - default: - return NAV_BAR_BOTTOM; - } - } - return NAV_BAR_INVALID; + boolean hasBottomNavigationBar() { + Insets navBarInsets = mDisplayContent.getInsetsStateController().getRawInsetsState() + .calculateInsets(mDisplayContent.mDisplayFrames.mUnrestricted, + Type.navigationBars(), true /* ignoreVisibilities */); + return navBarInsets.bottom > 0; } /** @@ -2405,7 +2387,7 @@ public class DisplayPolicy { return; } final WindowState navColorWin = chooseNavigationColorWindowLw(mNavBarColorWindowCandidate, - mDisplayContent.mInputMethodWindow, mNavigationBarPosition); + mDisplayContent.mInputMethodWindow, mHasBottomNavigationBar); final boolean isNavbarColorManagedByIme = navColorWin != null && navColorWin == mDisplayContent.mInputMethodWindow; final int appearance = updateLightNavigationBarLw(win.mAttrs.insetsFlags.appearance, @@ -2467,12 +2449,12 @@ public class DisplayPolicy { @VisibleForTesting @Nullable static WindowState chooseNavigationColorWindowLw(WindowState candidate, WindowState imeWindow, - @NavigationBarPosition int navBarPosition) { + boolean hasBottomNavigationBar) { // If the IME window is visible and FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is set, then IME // window can be navigation color window. final boolean imeWindowCanNavColorWindow = imeWindow != null && imeWindow.isVisible() - && navBarPosition == NAV_BAR_BOTTOM + && hasBottomNavigationBar && (imeWindow.mAttrs.flags & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; if (!imeWindowCanNavColorWindow) { @@ -2689,7 +2671,7 @@ public class DisplayPolicy { final WindowState navBackgroundWin = chooseNavigationBackgroundWindow( mNavBarBackgroundWindowCandidate, mDisplayContent.mInputMethodWindow, - mNavigationBarPosition); + mHasBottomNavigationBar); final boolean drawBackground = navBackgroundWin != null // There is no app window showing underneath nav bar. (e.g., The screen is locked.) // Let system windows (ex: notification shade) draw nav bar background. @@ -2727,8 +2709,8 @@ public class DisplayPolicy { @VisibleForTesting @Nullable static WindowState chooseNavigationBackgroundWindow(WindowState candidate, - WindowState imeWindow, @NavigationBarPosition int navBarPosition) { - if (imeWindow != null && imeWindow.isVisible() && navBarPosition == NAV_BAR_BOTTOM + WindowState imeWindow, boolean hasBottomNavigationBar) { + if (imeWindow != null && imeWindow.isVisible() && hasBottomNavigationBar && drawsBarBackground(imeWindow)) { return imeWindow; } @@ -2906,8 +2888,8 @@ public class DisplayPolicy { pw.print(prefix); pw.print("mNavigationBar="); pw.println(mNavigationBar); pw.print(prefix); pw.print("mNavBarOpacityMode="); pw.println(mNavBarOpacityMode); pw.print(prefix); pw.print("mNavigationBarCanMove="); pw.println(mNavigationBarCanMove); - pw.print(prefix); pw.print("mNavigationBarPosition="); - pw.println(mNavigationBarPosition); + pw.print(prefix); pw.print("mHasBottomNavigationBar="); + pw.println(mHasBottomNavigationBar); } if (mLeftGestureHost != null) { pw.print(prefix); pw.print("mLeftGestureHost="); pw.println(mLeftGestureHost); diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index a5085fc3147c..ea0b02c58974 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -191,7 +191,7 @@ cc_defaults { "android.hardware.power.stats@1.0", "android.hardware.power.stats-V1-ndk", "android.hardware.thermal@1.0", - "android.hardware.thermal-V2-ndk", + "android.hardware.thermal-V3-ndk", "android.hardware.tv.input@1.0", "android.hardware.tv.input-V2-ndk", "android.hardware.vibrator-V3-ndk", diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 248ed1a58b75..416e60f06c06 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -3056,6 +3056,12 @@ static void nativeSetMouseSwapPrimaryButtonEnabled(JNIEnv* env, jobject nativeIm im->setMouseSwapPrimaryButtonEnabled(enabled); } +static jboolean nativeSetKernelWakeEnabled(JNIEnv* env, jobject nativeImplObj, jint deviceId, + jboolean enabled) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + return im->getInputManager()->getReader().setKernelWakeEnabled(deviceId, enabled); +} + // ---------------------------------------------------------------------------- static const JNINativeMethod gInputManagerMethods[] = { @@ -3172,6 +3178,7 @@ static const JNINativeMethod gInputManagerMethods[] = { (void*)nativeSetAccessibilityStickyKeysEnabled}, {"setInputMethodConnectionIsActive", "(Z)V", (void*)nativeSetInputMethodConnectionIsActive}, {"getLastUsedInputDeviceId", "()I", (void*)nativeGetLastUsedInputDeviceId}, + {"setKernelWakeEnabled", "(IZ)Z", (void*)nativeSetKernelWakeEnabled}, }; #define FIND_CLASS(var, className) \ diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 2461c1ce3b0e..b87867a06cf1 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1399,6 +1399,10 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(BatteryService.class); t.traceEnd(); + t.traceBegin("StartTradeInModeService"); + mSystemServiceManager.startService(TradeInModeService.class); + t.traceEnd(); + // Tracks application usage stats. t.traceBegin("StartUsageService"); mSystemServiceManager.startService(UsageStatsService.class); diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java index aa9d8c6ea713..f1072da4161f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java @@ -18,6 +18,7 @@ package com.android.server.power; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -29,10 +30,16 @@ import android.hardware.thermal.TemperatureType; import android.hardware.thermal.ThrottlingSeverity; import android.os.Binder; import android.os.CoolingDevice; +import android.os.Flags; import android.os.RemoteException; import android.os.Temperature; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -40,16 +47,36 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; public class ThermalManagerServiceMockingTest { - @Mock private IThermal mAidlHalMock; + @ClassRule + public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule(); + + @Mock + private IThermal mAidlHalMock; private Binder mAidlBinder = new Binder(); private CompletableFuture<Temperature> mTemperatureFuture; - private ThermalManagerService.ThermalHalWrapper.TemperatureChangedCallback mTemperatureCallback; + private CompletableFuture<TemperatureThreshold> mThresholdFuture; + private ThermalManagerService.ThermalHalWrapper.WrapperThermalChangedCallback + mTemperatureCallback = + new ThermalManagerService.ThermalHalWrapper.WrapperThermalChangedCallback() { + @Override + public void onTemperatureChanged(Temperature temperature) { + mTemperatureFuture.complete(temperature); + } + + @Override + public void onThresholdChanged(TemperatureThreshold threshold) { + mThresholdFuture.complete(threshold); + } + }; private ThermalManagerService.ThermalHalAidlWrapper mAidlWrapper; @Captor ArgumentCaptor<IThermalChangedCallback> mAidlCallbackCaptor; @@ -60,27 +87,63 @@ public class ThermalManagerServiceMockingTest { Mockito.when(mAidlHalMock.asBinder()).thenReturn(mAidlBinder); mAidlBinder.attachInterface(mAidlHalMock, IThermal.class.getName()); mTemperatureFuture = new CompletableFuture<>(); - mTemperatureCallback = temperature -> mTemperatureFuture.complete(temperature); + mThresholdFuture = new CompletableFuture<>(); mAidlWrapper = new ThermalManagerService.ThermalHalAidlWrapper(mTemperatureCallback); mAidlWrapper.initProxyAndRegisterCallback(mAidlBinder); } @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK}) public void setCallback_aidl() throws Exception { Mockito.verify(mAidlHalMock, Mockito.times(1)).registerThermalChangedCallback( mAidlCallbackCaptor.capture()); - android.hardware.thermal.Temperature halT = + android.hardware.thermal.Temperature halTemperature = new android.hardware.thermal.Temperature(); - halT.type = TemperatureType.SOC; - halT.name = "test"; - halT.throttlingStatus = ThrottlingSeverity.SHUTDOWN; - halT.value = 99.0f; - mAidlCallbackCaptor.getValue().notifyThrottling(halT); + halTemperature.type = TemperatureType.SOC; + halTemperature.name = "test"; + halTemperature.throttlingStatus = ThrottlingSeverity.SHUTDOWN; + halTemperature.value = 99.0f; + + android.hardware.thermal.TemperatureThreshold halThreshold = + new android.hardware.thermal.TemperatureThreshold(); + halThreshold.type = TemperatureType.SKIN; + halThreshold.name = "test"; + halThreshold.hotThrottlingThresholds = new float[ThrottlingSeverity.SHUTDOWN + 1]; + Arrays.fill(halThreshold.hotThrottlingThresholds, Float.NaN); + halThreshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE] = 44.0f; + + mAidlCallbackCaptor.getValue().notifyThrottling(halTemperature); + mAidlCallbackCaptor.getValue().notifyThresholdChanged(halThreshold); + Temperature temperature = mTemperatureFuture.get(100, TimeUnit.MILLISECONDS); - assertEquals(halT.name, temperature.getName()); - assertEquals(halT.type, temperature.getType()); - assertEquals(halT.value, temperature.getValue(), 0.1f); - assertEquals(halT.throttlingStatus, temperature.getStatus()); + assertEquals(halTemperature.name, temperature.getName()); + assertEquals(halTemperature.type, temperature.getType()); + assertEquals(halTemperature.value, temperature.getValue(), 0.1f); + assertEquals(halTemperature.throttlingStatus, temperature.getStatus()); + + TemperatureThreshold threshold = mThresholdFuture.get(100, TimeUnit.MILLISECONDS); + assertEquals(halThreshold.name, threshold.name); + assertEquals(halThreshold.type, threshold.type); + assertArrayEquals(halThreshold.hotThrottlingThresholds, threshold.hotThrottlingThresholds, + 0.01f); + } + + @Test + @DisableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK}) + public void setCallback_aidl_allow_thermal_thresholds_callback_false() throws Exception { + Mockito.verify(mAidlHalMock, Mockito.times(1)).registerThermalChangedCallback( + mAidlCallbackCaptor.capture()); + android.hardware.thermal.TemperatureThreshold halThreshold = + new android.hardware.thermal.TemperatureThreshold(); + halThreshold.type = TemperatureType.SOC; + halThreshold.name = "test"; + halThreshold.hotThrottlingThresholds = new float[ThrottlingSeverity.SHUTDOWN + 1]; + Arrays.fill(halThreshold.hotThrottlingThresholds, Float.NaN); + halThreshold.hotThrottlingThresholds[ThrottlingSeverity.SEVERE] = 44.0f; + + mAidlCallbackCaptor.getValue().notifyThresholdChanged(halThreshold); + Thread.sleep(1000); + assertFalse(mThresholdFuture.isDone()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java index b7100ea00a40..c9e9f00985f1 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java @@ -15,6 +15,8 @@ */ package com.android.server.audio; +import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC; + import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -27,16 +29,20 @@ import static org.mockito.Mockito.when; import android.app.AppOpsManager; import android.content.Context; +import android.media.AudioDeviceAttributes; import android.media.AudioSystem; import android.os.Looper; import android.os.PermissionEnforcer; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; import androidx.test.runner.AndroidJUnit4; +import com.android.media.flags.Flags; + import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -53,6 +59,7 @@ public class AudioServiceTest { private static final String TAG = "AudioServiceTest"; private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100; + private static final int DEFAULT_INPUT_GAIN_INDEX = 50; @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -202,4 +209,29 @@ public class AudioServiceTest { reset(mSpySystemServer); } } + + /** Test input gain index setter and getter */ + @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL) + @Test + public void testInputGainIndex() throws Exception { + Log.i(TAG, "running testInputGainIndex"); + Assert.assertNotNull(mAudioService); + Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS); // wait for full AudioService initialization + + AudioDeviceAttributes ada = + new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_INPUT, TYPE_BUILTIN_MIC, /* address= */ ""); + + Assert.assertEquals( + "default input gain index reporting wrong value", + DEFAULT_INPUT_GAIN_INDEX, + mAudioService.getInputGainIndex(ada)); + + int inputGainIndex = 20; + mAudioService.setInputGainIndex(ada, inputGainIndex); + Assert.assertEquals( + "input gain index reporting wrong value", + inputGainIndex, + mAudioService.getInputGainIndex(ada)); + } } diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java index 6d79ae467bf0..cfe3d84140df 100644 --- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java @@ -296,11 +296,11 @@ public class ThermalManagerServiceTest { } @Test - public void testNotify() throws RemoteException { + public void testNotifyThrottling() throws RemoteException { int status = Temperature.THROTTLING_SEVERE; // Should only notify event not status Temperature newBattery = new Temperature(50, Temperature.TYPE_BATTERY, "batt", status); - mFakeHal.mCallback.onValues(newBattery); + mFakeHal.mCallback.onTemperatureChanged(newBattery); verify(mEventListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC) .times(1)).notifyThrottling(newBattery); verify(mStatusListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC) @@ -312,7 +312,7 @@ public class ThermalManagerServiceTest { resetListenerMock(); // Notify both event and status Temperature newSkin = new Temperature(50, Temperature.TYPE_SKIN, "skin1", status); - mFakeHal.mCallback.onValues(newSkin); + mFakeHal.mCallback.onTemperatureChanged(newSkin); verify(mEventListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC) .times(1)).notifyThrottling(newSkin); verify(mStatusListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC) @@ -325,7 +325,7 @@ public class ThermalManagerServiceTest { // Back to None, should only notify event not status status = Temperature.THROTTLING_NONE; newBattery = new Temperature(50, Temperature.TYPE_BATTERY, "batt", status); - mFakeHal.mCallback.onValues(newBattery); + mFakeHal.mCallback.onTemperatureChanged(newBattery); verify(mEventListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC) .times(1)).notifyThrottling(newBattery); verify(mStatusListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC) @@ -337,7 +337,7 @@ public class ThermalManagerServiceTest { resetListenerMock(); // Should also notify status newSkin = new Temperature(50, Temperature.TYPE_SKIN, "skin1", status); - mFakeHal.mCallback.onValues(newSkin); + mFakeHal.mCallback.onTemperatureChanged(newSkin); verify(mEventListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC) .times(1)).notifyThrottling(newSkin); verify(mStatusListener1, timeout(CALLBACK_TIMEOUT_MILLI_SEC) @@ -362,7 +362,7 @@ public class ThermalManagerServiceTest { public void testGetCurrentStatus() throws RemoteException { int status = Temperature.THROTTLING_SEVERE; Temperature newSkin = new Temperature(100, Temperature.TYPE_SKIN, "skin1", status); - mFakeHal.mCallback.onValues(newSkin); + mFakeHal.mCallback.onTemperatureChanged(newSkin); assertEquals(status, mService.mService.getCurrentThermalStatus()); int battStatus = Temperature.THROTTLING_EMERGENCY; Temperature newBattery = new Temperature(60, Temperature.TYPE_BATTERY, "batt", battStatus); @@ -373,11 +373,11 @@ public class ThermalManagerServiceTest { public void testThermalShutdown() throws RemoteException { int status = Temperature.THROTTLING_SHUTDOWN; Temperature newSkin = new Temperature(100, Temperature.TYPE_SKIN, "skin1", status); - mFakeHal.mCallback.onValues(newSkin); + mFakeHal.mCallback.onTemperatureChanged(newSkin); verify(mIPowerManagerMock, timeout(CALLBACK_TIMEOUT_MILLI_SEC) .times(1)).shutdown(false, PowerManager.SHUTDOWN_THERMAL_STATE, false); Temperature newBattery = new Temperature(60, Temperature.TYPE_BATTERY, "batt", status); - mFakeHal.mCallback.onValues(newBattery); + mFakeHal.mCallback.onTemperatureChanged(newBattery); verify(mIPowerManagerMock, timeout(CALLBACK_TIMEOUT_MILLI_SEC) .times(1)).shutdown(false, PowerManager.SHUTDOWN_BATTERY_THERMAL_STATE, false); } @@ -419,15 +419,35 @@ public class ThermalManagerServiceTest { } @Test - public void testTemperatureWatcherUpdateSevereThresholds() throws RemoteException { + public void testTemperatureWatcherUpdateSevereThresholds() { TemperatureWatcher watcher = mService.mTemperatureWatcher; - watcher.mSevereThresholds.erase(); - watcher.updateThresholds(); - assertEquals(1, watcher.mSevereThresholds.size()); - assertEquals("skin1", watcher.mSevereThresholds.keyAt(0)); - Float threshold = watcher.mSevereThresholds.get("skin1"); - assertNotNull(threshold); - assertEquals(40.0f, threshold, 0.0f); + synchronized (watcher.mSamples) { + watcher.mSevereThresholds.erase(); + watcher.getAndUpdateThresholds(); + assertEquals(1, watcher.mSevereThresholds.size()); + assertEquals("skin1", watcher.mSevereThresholds.keyAt(0)); + Float threshold = watcher.mSevereThresholds.get("skin1"); + assertNotNull(threshold); + assertEquals(40.0f, threshold, 0.0f); + assertArrayEquals("Got" + Arrays.toString(watcher.mHeadroomThresholds), + new float[]{Float.NaN, 0.6667f, 0.8333f, 1.0f, 1.166f, 1.3333f, + 1.5f}, + watcher.mHeadroomThresholds, 0.01f); + + TemperatureThreshold newThreshold = new TemperatureThreshold(); + newThreshold.name = "skin1"; + newThreshold.hotThrottlingThresholds = new float[] { + Float.NaN, 44.0f, 47.0f, 50.0f, Float.NaN, Float.NaN, Float.NaN + }; + mFakeHal.mCallback.onThresholdChanged(newThreshold); + threshold = watcher.mSevereThresholds.get("skin1"); + assertNotNull(threshold); + assertEquals(50.0f, threshold, 0.0f); + assertArrayEquals("Got" + Arrays.toString(watcher.mHeadroomThresholds), + new float[]{Float.NaN, 0.8f, 0.9f, 1.0f, Float.NaN, Float.NaN, + Float.NaN}, + watcher.mHeadroomThresholds, 0.01f); + } } @Test @@ -436,25 +456,19 @@ public class ThermalManagerServiceTest { synchronized (watcher.mSamples) { Arrays.fill(watcher.mHeadroomThresholds, Float.NaN); } - watcher.updateHeadroomThreshold(ThrottlingSeverity.LIGHT, 40, 49); - watcher.updateHeadroomThreshold(ThrottlingSeverity.MODERATE, 46, 49); - watcher.updateHeadroomThreshold(ThrottlingSeverity.SEVERE, 49, 49); - watcher.updateHeadroomThreshold(ThrottlingSeverity.CRITICAL, 64, 49); - watcher.updateHeadroomThreshold(ThrottlingSeverity.EMERGENCY, 70, 49); - watcher.updateHeadroomThreshold(ThrottlingSeverity.SHUTDOWN, 79, 49); + TemperatureThreshold threshold = new TemperatureThreshold(); + threshold.hotThrottlingThresholds = new float[]{Float.NaN, 40, 46, 49, 64, 70, 79}; synchronized (watcher.mSamples) { + watcher.updateTemperatureThresholdLocked(threshold, false /*override*/); assertArrayEquals(new float[]{Float.NaN, 0.7f, 0.9f, 1.0f, 1.5f, 1.7f, 2.0f}, watcher.mHeadroomThresholds, 0.01f); } // when another sensor reports different threshold, we expect to see smaller one to be used - watcher.updateHeadroomThreshold(ThrottlingSeverity.LIGHT, 37, 52); - watcher.updateHeadroomThreshold(ThrottlingSeverity.MODERATE, 46, 52); - watcher.updateHeadroomThreshold(ThrottlingSeverity.SEVERE, 52, 52); - watcher.updateHeadroomThreshold(ThrottlingSeverity.CRITICAL, 64, 52); - watcher.updateHeadroomThreshold(ThrottlingSeverity.EMERGENCY, 100, 52); - watcher.updateHeadroomThreshold(ThrottlingSeverity.SHUTDOWN, 200, 52); + threshold = new TemperatureThreshold(); + threshold.hotThrottlingThresholds = new float[]{Float.NaN, 37, 46, 52, 64, 100, 200}; synchronized (watcher.mSamples) { + watcher.updateTemperatureThresholdLocked(threshold, false /*override*/); assertArrayEquals(new float[]{Float.NaN, 0.5f, 0.8f, 1.0f, 1.4f, 1.7f, 2.0f}, watcher.mHeadroomThresholds, 0.01f); } @@ -486,7 +500,7 @@ public class ThermalManagerServiceTest { TemperatureWatcher watcher = mService.mTemperatureWatcher; ArrayList<TemperatureThreshold> thresholds = new ArrayList<>(); mFakeHal.mTemperatureThresholdList = thresholds; - watcher.updateThresholds(); + watcher.getAndUpdateThresholds(); synchronized (watcher.mSamples) { assertArrayEquals( new float[]{Float.NaN, Float.NaN, Float.NaN, Float.NaN, Float.NaN, Float.NaN, @@ -501,7 +515,7 @@ public class ThermalManagerServiceTest { Arrays.fill(nanThresholds.hotThrottlingThresholds, Float.NaN); Arrays.fill(nanThresholds.coldThrottlingThresholds, Float.NaN); thresholds.add(nanThresholds); - watcher.updateThresholds(); + watcher.getAndUpdateThresholds(); synchronized (watcher.mSamples) { assertArrayEquals( new float[]{Float.NaN, Float.NaN, Float.NaN, Float.NaN, Float.NaN, Float.NaN, diff --git a/services/tests/servicestests/src/com/android/server/power/WakefulnessSessionObserverTest.java b/services/tests/servicestests/src/com/android/server/power/WakefulnessSessionObserverTest.java index 6b32be0b2dfd..1af366b32da9 100644 --- a/services/tests/servicestests/src/com/android/server/power/WakefulnessSessionObserverTest.java +++ b/services/tests/servicestests/src/com/android/server/power/WakefulnessSessionObserverTest.java @@ -28,6 +28,7 @@ import static android.view.Display.DEFAULT_DISPLAY_GROUP; import static com.android.server.power.ScreenTimeoutOverridePolicy.RELEASE_REASON_UNKNOWN; import static com.android.server.power.ScreenTimeoutOverridePolicy.RELEASE_REASON_USER_ACTIVITY_TOUCH; import static com.android.server.power.WakefulnessSessionObserver.OFF_REASON_POWER_BUTTON; +import static com.android.server.power.WakefulnessSessionObserver.OFF_REASON_TIMEOUT; import static com.android.server.power.WakefulnessSessionObserver.OVERRIDE_OUTCOME_CANCEL_POWER_BUTTON; import static com.android.server.power.WakefulnessSessionObserver.OVERRIDE_OUTCOME_CANCEL_USER_INTERACTION; import static com.android.server.power.WakefulnessSessionObserver.OVERRIDE_OUTCOME_TIMEOUT_SUCCESS; @@ -40,6 +41,7 @@ import static com.android.server.power.WakefulnessSessionObserver.POLICY_REASON_ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -90,6 +92,7 @@ public class WakefulnessSessionObserverTest { mWakefulnessSessionFrameworkStatsLogger; @Mock private DisplayManagerInternal mDisplayManagerInternal; + private MockContentResolver mContentResolver = new MockContentResolver(); private TestHandler mHandler; @Before @@ -106,10 +109,9 @@ public class WakefulnessSessionObserverTest { R.integer.config_screenTimeoutOverride); when(mContext.getResources()).thenReturn(res); FakeSettingsProvider.clearSettingsProvider(); - MockContentResolver mockContentResolver = new MockContentResolver(); - mockContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); - when(mContext.getContentResolver()).thenReturn(mockContentResolver); - Settings.System.putIntForUser(mockContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + Settings.System.putIntForUser(mContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, DEFAULT_SCREEN_OFF_TIMEOUT_MS, UserHandle.USER_CURRENT); final DisplayInfo info = new DisplayInfo(); @@ -511,6 +513,115 @@ public class WakefulnessSessionObserverTest { DEFAULT_SCREEN_OFF_TIMEOUT_MS); // default Timeout Ms } + @Test + public void testScreenOffTimeout_normal_logSessionEventTriggered() { + int powerGroupId = 1; + int userActivity = PowerManager.USER_ACTIVITY_EVENT_ACCESSIBILITY; + triggerLogSessionEvent(powerGroupId, userActivity); + verify(mWakefulnessSessionFrameworkStatsLogger) + .logSessionEvent( + powerGroupId, // powerGroupId + OFF_REASON_TIMEOUT, // interactiveStateOffReason + 0, // interactiveStateOnDurationMs + userActivity, // userActivity + 0, // lastUserActivityEventDurationMs + DEFAULT_SCREEN_OFF_TIMEOUT_MS - OVERRIDE_SCREEN_OFF_TIMEOUT_MS + ); // reducedInteractiveStateOnDurationMs; + } + + @Test + public void testScreenOffTimeout_zero_noLogSessionEventTriggered() { + // simulate adding an invalid screen_off_timeout value + Settings.System.putIntForUser(mContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, + 0, // invalid timeout value + UserHandle.USER_CURRENT); + mWakefulnessSessionObserver.updateSettingScreenOffTimeout(mContext); + + try { + triggerLogSessionEvent(); + verify(mWakefulnessSessionFrameworkStatsLogger, never()) + .logSessionEvent(anyInt(), anyInt(), anyLong(), anyInt(), anyLong(), anyInt()); + } finally { + // rollback the original data + Settings.System.putIntForUser(mContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, + DEFAULT_SCREEN_OFF_TIMEOUT_MS, UserHandle.USER_CURRENT); + mWakefulnessSessionObserver.updateSettingScreenOffTimeout(mContext); + } + } + + @Test + public void testScreenOffTimeout_negative_noLogSessionEventTriggered() { + // simulate adding an invalid screen_off_timeout value + Settings.System.putIntForUser(mContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, + -1, // invalid timeout value + UserHandle.USER_CURRENT); + mWakefulnessSessionObserver.updateSettingScreenOffTimeout(mContext); + + try { + triggerLogSessionEvent(); + verify(mWakefulnessSessionFrameworkStatsLogger, never()) + .logSessionEvent(anyInt(), anyInt(), anyLong(), anyInt(), anyLong(), anyInt()); + } finally { + // rollback the original data + Settings.System.putIntForUser(mContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, + DEFAULT_SCREEN_OFF_TIMEOUT_MS, UserHandle.USER_CURRENT); + mWakefulnessSessionObserver.updateSettingScreenOffTimeout(mContext); + } + } + + @Test + public void testScreenOffTimeout_max_logSessionEventTriggered() { + // simulate adding the max screen_off_timeout value + int defaultTimeoutMs = Integer.MAX_VALUE; + Settings.System.putIntForUser(mContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, + defaultTimeoutMs, + UserHandle.USER_CURRENT); + mWakefulnessSessionObserver.updateSettingScreenOffTimeout(mContext); + + try { + int powerGroupId = 1; + int userActivity = PowerManager.USER_ACTIVITY_EVENT_ACCESSIBILITY; + triggerLogSessionEvent(powerGroupId, userActivity); + verify(mWakefulnessSessionFrameworkStatsLogger) + .logSessionEvent( + powerGroupId, // powerGroupId + OFF_REASON_TIMEOUT, // interactiveStateOffReason + 0, // interactiveStateOnDurationMs + userActivity, // userActivity + 0, // lastUserActivityEventDurationMs + defaultTimeoutMs - OVERRIDE_SCREEN_OFF_TIMEOUT_MS + ); // reducedInteractiveStateOnDurationMs; + } finally { + // rollback the original data + Settings.System.putIntForUser(mContentResolver, Settings.System.SCREEN_OFF_TIMEOUT, + DEFAULT_SCREEN_OFF_TIMEOUT_MS, UserHandle.USER_CURRENT); + mWakefulnessSessionObserver.updateSettingScreenOffTimeout(mContext); + } + } + + private void triggerLogSessionEvent() { + triggerLogSessionEvent(1, PowerManager.USER_ACTIVITY_EVENT_ACCESSIBILITY); + } + + private void triggerLogSessionEvent(int powerGroupId, int userActivity) { + mWakefulnessSessionObserver.onWakefulnessChangeStarted( + powerGroupId, + PowerManagerInternal.WAKEFULNESS_AWAKE, + WAKE_REASON_POWER_BUTTON, + mTestClock.now()); + mWakefulnessSessionObserver.onWakeLockAcquired( + PowerManager.SCREEN_TIMEOUT_OVERRIDE_WAKE_LOCK); + + long userActivityTime = mTestClock.now(); + mWakefulnessSessionObserver.notifyUserActivity( + userActivityTime, powerGroupId, userActivity); + mWakefulnessSessionObserver.onWakefulnessChangeStarted( + powerGroupId, + PowerManagerInternal.WAKEFULNESS_DOZING, + GO_TO_SLEEP_REASON_TIMEOUT, + mTestClock.now()); + } + private void advanceTime(long timeMs) { mTestClock.fastForward(timeMs); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java index fe4ce465e9be..52c34889a1ec 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java @@ -1,18 +1,38 @@ package com.android.server.notification; +import static android.app.AlarmManager.RTC_WAKEUP; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import static java.time.temporal.ChronoUnit.HOURS; +import static java.time.temporal.ChronoUnit.MINUTES; +import android.app.ActivityManager; +import android.app.AlarmManager; import android.app.Application; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; +import android.os.Bundle; +import android.os.SimpleClock; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.Condition; import android.service.notification.ScheduleCalendar; import android.service.notification.ZenModeConfig; @@ -24,11 +44,20 @@ import androidx.test.filters.SmallTest; import com.android.server.UiServiceTestCase; import com.android.server.pm.PackageManagerService; +import com.google.common.collect.ImmutableList; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.GregorianCalendar; @@ -36,17 +65,22 @@ import java.util.GregorianCalendar; @SmallTest @RunWithLooper public class ScheduleConditionProviderTest extends UiServiceTestCase { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - ScheduleConditionProvider mService; + private ScheduleConditionProvider mService; + private TestClock mClock = new TestClock(); + @Mock private AlarmManager mAlarmManager; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mContext.addMockSystemService(AlarmManager.class, mAlarmManager); Intent startIntent = new Intent("com.android.server.notification.ScheduleConditionProvider"); startIntent.setPackage("android"); - ScheduleConditionProvider service = new ScheduleConditionProvider(); + ScheduleConditionProvider service = new ScheduleConditionProvider(mClock); service.attach( getContext(), null, // ActivityThread not actually used in Service @@ -57,7 +91,7 @@ public class ScheduleConditionProviderTest extends UiServiceTestCase { ); service.onCreate(); service.onBind(startIntent); - mService = spy(service); + mService = service; } @Test @@ -343,6 +377,87 @@ public class ScheduleConditionProviderTest extends UiServiceTestCase { assertEquals(PackageManagerService.PLATFORM_PACKAGE_NAME, pi.getIntent().getPackage()); } + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_HSUM) + public void onSubscribe_registersReceiverForAllUsers() { + Calendar now = getNow(); + Uri condition = ZenModeConfig.toScheduleConditionId(getScheduleEndsInHour(now)); + + mService.onSubscribe(condition); + + ArgumentCaptor<IntentFilter> filterCaptor = ArgumentCaptor.forClass(IntentFilter.class); + verify(mContext).registerReceiverForAllUsers(any(), filterCaptor.capture(), any(), any()); + IntentFilter filter = filterCaptor.getValue(); + assertThat(filter.actionsIterator()).isNotNull(); + assertThat(ImmutableList.copyOf(filter.actionsIterator())) + .contains(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_HSUM) + public void onAlarmClockChanged_storesNextAlarm() { + Instant scheduleStart = Instant.parse("2024-10-22T16:00:00Z"); + Instant scheduleEnd = scheduleStart.plus(1, HOURS); + + Instant now = scheduleStart.plus(15, MINUTES); + mClock.setNowMillis(now.toEpochMilli()); + + Uri condition = ZenModeConfig.toScheduleConditionId( + getOneHourSchedule(scheduleStart.atZone(ZoneId.systemDefault()))); + mService.onSubscribe(condition); + + // Now prepare to send an "alarm set for 16:30" broadcast. + Instant alarm = scheduleStart.plus(30, MINUTES); + ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass( + BroadcastReceiver.class); + verify(mContext).registerReceiverForAllUsers(receiverCaptor.capture(), any(), any(), any()); + BroadcastReceiver receiver = receiverCaptor.getValue(); + receiver.setPendingResult(pendingResultForUserBroadcast(ActivityManager.getCurrentUser())); + when(mAlarmManager.getNextAlarmClock(anyInt())).thenReturn( + new AlarmManager.AlarmClockInfo(alarm.toEpochMilli(), null)); + + Intent intent = new Intent(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); + receiver.onReceive(mContext, intent); + + // The time for the alarm was stored in the ScheduleCalendar, meaning the rule will end when + // the next evaluation after that point happens. + ScheduleCalendar scheduleCalendar = + mService.getSubscriptions().values().stream().findFirst().get(); + assertThat(scheduleCalendar.shouldExitForAlarm(alarm.toEpochMilli() - 1)).isFalse(); + assertThat(scheduleCalendar.shouldExitForAlarm(alarm.toEpochMilli() + 1)).isTrue(); + + // But the next wakeup is unchanged, at the time of the schedule end (17:00). + verify(mAlarmManager, times(2)).setExact(eq(RTC_WAKEUP), eq(scheduleEnd.toEpochMilli()), + any()); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_HSUM) + public void onAlarmClockChanged_forAnotherUser_isIgnored() { + Instant scheduleStart = Instant.parse("2024-10-22T16:00:00Z"); + Instant now = scheduleStart.plus(15, MINUTES); + mClock.setNowMillis(now.toEpochMilli()); + + Uri condition = ZenModeConfig.toScheduleConditionId( + getOneHourSchedule(scheduleStart.atZone(ZoneId.systemDefault()))); + mService.onSubscribe(condition); + + // Now prepare to send an "alarm set for a different user" broadcast. + ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass( + BroadcastReceiver.class); + verify(mContext).registerReceiverForAllUsers(receiverCaptor.capture(), any(), any(), any()); + BroadcastReceiver receiver = receiverCaptor.getValue(); + + reset(mAlarmManager); + int anotherUser = ActivityManager.getCurrentUser() + 1; + receiver.setPendingResult(pendingResultForUserBroadcast(anotherUser)); + Intent intent = new Intent(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); + receiver.onReceive(mContext, intent); + + // The alarm data was not read. + verify(mAlarmManager, never()).getNextAlarmClock(anyInt()); + } + private Calendar getNow() { Calendar now = new GregorianCalendar(); now.set(Calendar.HOUR_OF_DAY, 14); @@ -363,4 +478,38 @@ public class ScheduleConditionProviderTest extends UiServiceTestCase { info.endMinute = now.get(Calendar.MINUTE); return info; } + + private static ZenModeConfig.ScheduleInfo getOneHourSchedule(ZonedDateTime start) { + ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo(); + // Note: DayOfWeek.MONDAY doesn't match Calendar.MONDAY + info.days = new int[] { (start.getDayOfWeek().getValue() % 7) + 1 }; + info.startHour = start.getHour(); + info.startMinute = start.getMinute(); + info.endHour = start.plusHours(1).getHour(); + info.endMinute = start.plusHours(1).getMinute(); + info.exitAtAlarm = true; + return info; + } + + private static BroadcastReceiver.PendingResult pendingResultForUserBroadcast(int userId) { + return new BroadcastReceiver.PendingResult(0, "", new Bundle(), 0, false, false, null, + userId, 0); + } + + private static class TestClock extends SimpleClock { + private long mNowMillis = 441644400000L; + + private TestClock() { + super(ZoneOffset.UTC); + } + + @Override + public long millis() { + return mNowMillis; + } + + private void setNowMillis(long millis) { + mNowMillis = millis; + } + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index 3bd57475614f..eb8bc9125034 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -40,7 +40,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_BOTTOM; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -141,49 +140,49 @@ public class DisplayPolicyTests extends WindowTestsBase { // If everything is null, return null. assertNull(null, DisplayPolicy.chooseNavigationColorWindowLw( - null, null, NAV_BAR_BOTTOM)); + null, null, true)); // If no IME windows, return candidate window. assertEquals(candidate, DisplayPolicy.chooseNavigationColorWindowLw( - candidate, null, NAV_BAR_BOTTOM)); + candidate, null, true)); assertEquals(dimmingImTarget, DisplayPolicy.chooseNavigationColorWindowLw( - dimmingImTarget, null, NAV_BAR_BOTTOM)); + dimmingImTarget, null, true)); assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw( - dimmingNonImTarget, null, NAV_BAR_BOTTOM)); + dimmingNonImTarget, null, true)); // If IME is not visible, return candidate window. assertEquals(null, DisplayPolicy.chooseNavigationColorWindowLw( - null, invisibleIme, NAV_BAR_BOTTOM)); + null, invisibleIme, true)); assertEquals(candidate, DisplayPolicy.chooseNavigationColorWindowLw( - candidate, invisibleIme, NAV_BAR_BOTTOM)); + candidate, invisibleIme, true)); assertEquals(dimmingImTarget, DisplayPolicy.chooseNavigationColorWindowLw( - dimmingImTarget, invisibleIme, NAV_BAR_BOTTOM)); + dimmingImTarget, invisibleIme, true)); assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw( - dimmingNonImTarget, invisibleIme, NAV_BAR_BOTTOM)); + dimmingNonImTarget, invisibleIme, true)); // If IME is visible, return candidate when the candidate window is not dimming. assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw( - null, visibleIme, NAV_BAR_BOTTOM)); + null, visibleIme, true)); assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw( - candidate, visibleIme, NAV_BAR_BOTTOM)); + candidate, visibleIme, true)); // If IME is visible and the candidate window is dimming, checks whether the dimming window // can be IME tartget or not. assertEquals(visibleIme, DisplayPolicy.chooseNavigationColorWindowLw( - dimmingImTarget, visibleIme, NAV_BAR_BOTTOM)); + dimmingImTarget, visibleIme, true)); assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw( - dimmingNonImTarget, visibleIme, NAV_BAR_BOTTOM)); + dimmingNonImTarget, visibleIme, true)); // Only IME windows that have FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS should be navigation color // window. assertEquals(null, DisplayPolicy.chooseNavigationColorWindowLw( - null, imeNonDrawNavBar, NAV_BAR_BOTTOM)); + null, imeNonDrawNavBar, true)); assertEquals(candidate, DisplayPolicy.chooseNavigationColorWindowLw( - candidate, imeNonDrawNavBar, NAV_BAR_BOTTOM)); + candidate, imeNonDrawNavBar, true)); assertEquals(dimmingImTarget, DisplayPolicy.chooseNavigationColorWindowLw( - dimmingImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM)); + dimmingImTarget, imeNonDrawNavBar, true)); assertEquals(dimmingNonImTarget, DisplayPolicy.chooseNavigationColorWindowLw( - dimmingNonImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM)); + dimmingNonImTarget, imeNonDrawNavBar, true)); } @Test @@ -196,32 +195,32 @@ public class DisplayPolicyTests extends WindowTestsBase { final WindowState nonDrawBarIme = createInputMethodWindow(true, false, false); assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow( - drawBarWin, null, NAV_BAR_BOTTOM)); + drawBarWin, null, true)); assertNull(DisplayPolicy.chooseNavigationBackgroundWindow( - null, null, NAV_BAR_BOTTOM)); + null, null, true)); assertNull(DisplayPolicy.chooseNavigationBackgroundWindow( - nonDrawBarWin, null, NAV_BAR_BOTTOM)); + nonDrawBarWin, null, true)); assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow( - drawBarWin, visibleIme, NAV_BAR_BOTTOM)); + drawBarWin, visibleIme, true)); assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow( - null, visibleIme, NAV_BAR_BOTTOM)); + null, visibleIme, true)); assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow( - nonDrawBarWin, visibleIme, NAV_BAR_BOTTOM)); + nonDrawBarWin, visibleIme, true)); assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow( - drawBarWin, invisibleIme, NAV_BAR_BOTTOM)); + drawBarWin, invisibleIme, true)); assertNull(DisplayPolicy.chooseNavigationBackgroundWindow( - null, invisibleIme, NAV_BAR_BOTTOM)); + null, invisibleIme, true)); assertNull(DisplayPolicy.chooseNavigationBackgroundWindow( - nonDrawBarWin, invisibleIme, NAV_BAR_BOTTOM)); + nonDrawBarWin, invisibleIme, true)); assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow( - drawBarWin, nonDrawBarIme, NAV_BAR_BOTTOM)); + drawBarWin, nonDrawBarIme, true)); assertNull(DisplayPolicy.chooseNavigationBackgroundWindow( - null, nonDrawBarIme, NAV_BAR_BOTTOM)); + null, nonDrawBarIme, true)); assertNull(DisplayPolicy.chooseNavigationBackgroundWindow( - nonDrawBarWin, nonDrawBarIme, NAV_BAR_BOTTOM)); + nonDrawBarWin, nonDrawBarIme, true)); } @SetupWindows(addWindows = W_NAVIGATION_BAR) diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index 00ecd008cde7..546b1ba66b08 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -19,11 +19,9 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; import static android.view.Surface.ROTATION_0; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -208,7 +206,7 @@ class TestDisplayContent extends DisplayContent { if (mSystemDecorations) { doReturn(true).when(newDisplay).supportsSystemDecorations(); doReturn(true).when(displayPolicy).hasNavigationBar(); - doReturn(NAV_BAR_BOTTOM).when(displayPolicy).navigationBarPosition(anyInt()); + doReturn(true).when(displayPolicy).hasBottomNavigationBar(); } else { doReturn(false).when(displayPolicy).hasNavigationBar(); doReturn(false).when(displayPolicy).hasStatusBar(); diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 79b3a7c4de65..94be3d409401 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -127,7 +127,9 @@ public final class SatelliteManager { /** * Exception from the satellite service containing the {@link SatelliteResult} error code. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static class SatelliteException extends Exception { @SatelliteResult private final int mErrorCode; @@ -257,140 +259,210 @@ public final class SatelliteManager { /** * The request was successfully processed. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_SUCCESS = 0; + /** * A generic error which should be used only when other specific errors cannot be used. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_ERROR = 1; + /** * Error received from the satellite server. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_SERVER_ERROR = 2; + /** * Error received from the vendor service. This generic error code should be used * only when the error cannot be mapped to other specific service error codes. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_SERVICE_ERROR = 3; + /** * Error received from satellite modem. This generic error code should be used only when * the error cannot be mapped to other specific modem error codes. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_MODEM_ERROR = 4; + /** * Error received from the satellite network. This generic error code should be used only when * the error cannot be mapped to other specific network error codes. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_NETWORK_ERROR = 5; + /** * Telephony is not in a valid state to receive requests from clients. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; + /** * Satellite modem is not in a valid state to receive requests from clients. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_INVALID_MODEM_STATE = 7; + /** * Either vendor service, or modem, or Telephony framework has received a request with * invalid arguments from its clients. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_INVALID_ARGUMENTS = 8; + /** * Telephony framework failed to send a request or receive a response from the vendor service * or satellite modem due to internal error. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_REQUEST_FAILED = 9; + /** * Radio did not start or is resetting. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_RADIO_NOT_AVAILABLE = 10; + /** * The request is not supported by either the satellite modem or the network. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_REQUEST_NOT_SUPPORTED = 11; + /** * Satellite modem or network has no resources available to handle requests from clients. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_NO_RESOURCES = 12; + /** * Satellite service is not provisioned yet. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_SERVICE_NOT_PROVISIONED = 13; + /** * Satellite service provision is already in progress. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_SERVICE_PROVISION_IN_PROGRESS = 14; + /** * The ongoing request was aborted by either the satellite modem or the network. * This error is also returned when framework decides to abort current send request as one * of the previous send request failed. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_REQUEST_ABORTED = 15; + /** * The device/subscriber is barred from accessing the satellite service. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_ACCESS_BARRED = 16; + /** * Satellite modem timeout to receive ACK or response from the satellite network after * sending a request to the network. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_NETWORK_TIMEOUT = 17; + /** * Satellite network is not reachable from the modem. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_NOT_REACHABLE = 18; + /** * The device/subscriber is not authorized to register with the satellite service provider. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_NOT_AUTHORIZED = 19; + /** * The device does not support satellite. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_NOT_SUPPORTED = 20; /** * The current request is already in-progress. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_REQUEST_IN_PROGRESS = 21; /** * Satellite modem is currently busy due to which current request cannot be processed. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_MODEM_BUSY = 22; /** * Telephony process is not currently available or satellite is not supported. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; /** * Telephony framework timeout to receive ACK or response from the satellite modem after * sending a request to the modem. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_MODEM_TIMEOUT = 24; @@ -475,27 +547,41 @@ public final class SatelliteManager { /** * Unknown Non-Terrestrial radio technology. This generic radio technology should be used * only when the radio technology cannot be mapped to other specific radio technologies. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int NT_RADIO_TECHNOLOGY_UNKNOWN = 0; + /** * 3GPP NB-IoT (Narrowband Internet of Things) over Non-Terrestrial-Networks technology. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int NT_RADIO_TECHNOLOGY_NB_IOT_NTN = 1; + /** * 3GPP 5G NR over Non-Terrestrial-Networks technology. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int NT_RADIO_TECHNOLOGY_NR_NTN = 2; + /** * 3GPP eMTC (enhanced Machine-Type Communication) over Non-Terrestrial-Networks technology. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int NT_RADIO_TECHNOLOGY_EMTC_NTN = 3; + /** * Proprietary technology. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int NT_RADIO_TECHNOLOGY_PROPRIETARY = 4; @@ -510,16 +596,35 @@ public final class SatelliteManager { @Retention(RetentionPolicy.SOURCE) public @interface NTRadioTechnology {} - /** Suggested device hold position is unknown. */ + /** + * Suggested device hold position is unknown. + * @hide + */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DEVICE_HOLD_POSITION_UNKNOWN = 0; - /** User is suggested to hold the device in portrait mode. */ + + /** + * User is suggested to hold the device in portrait mode. + * @hide + */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DEVICE_HOLD_POSITION_PORTRAIT = 1; - /** User is suggested to hold the device in landscape mode with left hand. */ + + /** + * User is suggested to hold the device in landscape mode with left hand. + * @hide + */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DEVICE_HOLD_POSITION_LANDSCAPE_LEFT = 2; - /** User is suggested to hold the device in landscape mode with right hand. */ + + /** + * User is suggested to hold the device in landscape mode with right hand. + * @hide + */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DEVICE_HOLD_POSITION_LANDSCAPE_RIGHT = 3; @@ -533,18 +638,37 @@ public final class SatelliteManager { @Retention(RetentionPolicy.SOURCE) public @interface DeviceHoldPosition {} - /** Display mode is unknown. */ + /** + * Display mode is unknown. + * @hide + */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DISPLAY_MODE_UNKNOWN = 0; - /** Display mode of the device used for satellite communication for non-foldable phones. */ + + /** + * Display mode of the device used for satellite communication for non-foldable phones. + * @hide + */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DISPLAY_MODE_FIXED = 1; - /** Display mode of the device used for satellite communication for foldabale phones when the - * device is opened. */ + + /** + * Display mode of the device used for satellite communication for foldabale phones when the + * device is opened. + * @hide + */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DISPLAY_MODE_OPENED = 2; - /** Display mode of the device used for satellite communication for foldabable phones when the - * device is closed. */ + + /** + * Display mode of the device used for satellite communication for foldabable phones when the + * device is closed. + * @hide + */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DISPLAY_MODE_CLOSED = 3; @@ -561,13 +685,18 @@ public final class SatelliteManager { /** * The emergency call is handed over to oem-enabled satellite SOS messaging. SOS messages are * sent to SOS providers, which will then forward the messages to emergency providers. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_SOS = 1; + /** * The emergency call is handed over to carrier-enabled satellite T911 messaging. T911 messages * are sent directly to local emergency providers. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final int EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911 = 2; @@ -614,7 +743,10 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestEnabled(@NonNull EnableRequestAttributes attributes, @@ -660,7 +792,10 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestIsEnabled(@NonNull @CallbackExecutor Executor executor, @@ -717,7 +852,10 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestIsDemoModeEnabled(@NonNull @CallbackExecutor Executor executor, @@ -774,7 +912,10 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestIsEmergencyModeEnabled(@NonNull @CallbackExecutor Executor executor, @@ -832,7 +973,10 @@ public final class SatelliteManager { * service is supported on the device and {@code false} otherwise. * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} * will return a {@link SatelliteException} with the {@link SatelliteResult}. + * + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestIsSupported(@NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { @@ -887,7 +1031,10 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestCapabilities(@NonNull @CallbackExecutor Executor executor, @@ -936,56 +1083,80 @@ public final class SatelliteManager { /** * The default state indicating that datagram transfer is idle. * This should be sent if there are no message transfer activity happening. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE = 0; + /** * A transition state indicating that a datagram is being sent. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SENDING = 1; + /** * An end state indicating that datagram sending completed successfully. * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE} * will be sent if no more messages are pending. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_SUCCESS = 2; + /** * An end state indicating that datagram sending completed with a failure. * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE} * must be sent before reporting any additional datagram transfer state changes. All pending * messages will be reported as failed, to the corresponding applications. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED = 3; + /** * A transition state indicating that a datagram is being received. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVING = 4; + /** * An end state indicating that datagram receiving completed successfully. * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE} * will be sent if no more messages are pending. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_SUCCESS = 5; + /** * An end state indicating that datagram receive operation found that there are no * messages to be retrieved from the satellite. * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE} * will be sent if no more messages are pending. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_NONE = 6; + /** * An end state indicating that datagram receive completed with a failure. * After datagram transfer completes, {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE} * will be sent if no more messages are pending. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED = 7; + /** * A transition state indicating that Telephony is waiting for satellite modem to connect to a * satellite network before sending a datagram or polling for datagrams. If the satellite modem @@ -994,14 +1165,19 @@ public final class SatelliteManager { * {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVING} will be sent. Otherwise, * either {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED} or * {@link #SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED} will be sent. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_WAITING_TO_CONNECT = 8; + /** * The datagram transfer state is unknown. This generic datagram transfer state should be used * only when the datagram transfer state cannot be mapped to other specific datagram transfer * states. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_UNKNOWN = -1; @@ -1024,58 +1200,86 @@ public final class SatelliteManager { /** * Satellite modem is in idle state. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_IDLE = 0; + /** * Satellite modem is listening for incoming datagrams. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_LISTENING = 1; + /** * Satellite modem is sending and/or receiving datagrams. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING = 2; + /** * Satellite modem is retrying to send and/or receive datagrams. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_DATAGRAM_RETRYING = 3; + /** * Satellite modem is powered off. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_OFF = 4; + /** * Satellite modem is unavailable. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_UNAVAILABLE = 5; + /** * The satellite modem is powered on but the device is not registered to a satellite cell. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_NOT_CONNECTED = 6; + /** * The satellite modem is powered on and the device is registered to a satellite cell. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_CONNECTED = 7; + /** * The satellite modem is being powered on. * @hide */ public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8; + /** * The satellite modem is being powered off. * @hide */ public static final int SATELLITE_MODEM_STATE_DISABLING_SATELLITE = 9; + /** * Satellite modem state is unknown. This generic modem state should be used only when the * modem state cannot be mapped to other specific modem states. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1; @@ -1099,43 +1303,56 @@ public final class SatelliteManager { /** * Datagram type is unknown. This generic datagram type should be used only when the * datagram type cannot be mapped to other specific datagram types. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DATAGRAM_TYPE_UNKNOWN = 0; + /** * Datagram type indicating that the datagram to be sent or received is of type SOS message. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DATAGRAM_TYPE_SOS_MESSAGE = 1; + /** * Datagram type indicating that the datagram to be sent or received is of type * location sharing. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2; + /** * This type of datagram is used to keep the device in satellite connected state or check if * there is any incoming message. * @hide */ public static final int DATAGRAM_TYPE_KEEP_ALIVE = 3; + /** * Datagram type indicating that the datagram to be sent or received is of type SOS message and * is the last message to emergency service provider indicating still needs help. * @hide */ public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_STILL_NEED_HELP = 4; + /** * Datagram type indicating that the datagram to be sent or received is of type SOS message and * is the last message to emergency service provider indicating no more help is needed. * @hide */ public static final int DATAGRAM_TYPE_LAST_SOS_MESSAGE_NO_HELP_NEEDED = 5; + /** * Datagram type indicating that the message to be sent or received is of type SMS. * @hide */ public static final int DATAGRAM_TYPE_SMS = 6; + /** * Datagram type indicating that the message to be sent is an SMS checking * for pending incoming SMS. @@ -1166,7 +1383,9 @@ public final class SatelliteManager { /** * Satellite communication restricted by geolocation. This can be * triggered based upon geofence input provided by carrier to enable or disable satellite. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; @@ -1174,7 +1393,9 @@ public final class SatelliteManager { * Satellite communication restricted by entitlement server. This can be triggered based on * the EntitlementStatus value received from the entitlement server to enable or disable * satellite. + * @hide */ + @SystemApi @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT = 2; @@ -1201,7 +1422,10 @@ public final class SatelliteManager { * @param callback The callback to notify of satellite transmission updates. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @SuppressWarnings("SamShouldBeLast") @@ -1284,7 +1508,10 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void stopTransmissionUpdates(@NonNull SatelliteTransmissionUpdateCallback callback, @@ -1342,7 +1569,10 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void provisionService(@NonNull String token, @NonNull byte[] provisionData, @@ -1397,7 +1627,10 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void deprovisionService(@NonNull String token, @@ -1440,7 +1673,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @SatelliteResult public int registerForProvisionStateChanged( @@ -1492,7 +1728,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void unregisterForProvisionStateChanged( @@ -1530,7 +1769,10 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestIsProvisioned(@NonNull @CallbackExecutor Executor executor, @@ -1585,7 +1827,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @SatelliteResult public int registerForModemStateChanged( @@ -1639,7 +1884,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void unregisterForModemStateChanged( @@ -1679,7 +1927,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @SatelliteResult public int registerForIncomingDatagram( @@ -1735,7 +1986,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void unregisterForIncomingDatagram(@NonNull SatelliteDatagramCallback callback) { @@ -1773,7 +2027,10 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void pollPendingDatagrams(@NonNull @CallbackExecutor Executor executor, @@ -1828,7 +2085,10 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void sendDatagram(@DatagramType int datagramType, @@ -1876,7 +2136,10 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestIsCommunicationAllowedForCurrentLocation( @@ -1934,7 +2197,10 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestTimeForNextSatelliteVisibility(@NonNull @CallbackExecutor Executor executor, @@ -1992,7 +2258,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void setDeviceAlignedWithSatellite(boolean isAligned) { @@ -2032,7 +2301,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalArgumentException if the subscription is invalid. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public void requestAttachEnabledForCarrier(int subId, boolean enableSatellite, @@ -2066,7 +2338,10 @@ public final class SatelliteManager { * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if the subscription is invalid. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public void requestIsAttachEnabledForCarrier(int subId, @@ -2091,7 +2366,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalArgumentException if the subscription is invalid. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public void addAttachRestrictionForCarrier(int subId, @@ -2136,7 +2414,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalArgumentException if the subscription is invalid. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public void removeAttachRestrictionForCarrier(int subId, @@ -2180,7 +2461,10 @@ public final class SatelliteManager { * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if the subscription is invalid. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @SatelliteCommunicationRestrictionReason @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) @@ -2230,7 +2514,10 @@ public final class SatelliteManager { * {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestNtnSignalStrength(@NonNull @CallbackExecutor Executor executor, @@ -2293,7 +2580,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void registerForNtnSignalStrengthChanged(@NonNull @CallbackExecutor Executor executor, @@ -2342,7 +2632,10 @@ public final class SatelliteManager { * @throws IllegalArgumentException if the callback is not valid or has already been * unregistered. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void unregisterForNtnSignalStrengthChanged(@NonNull NtnSignalStrengthCallback callback) { @@ -2376,7 +2669,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @SatelliteResult public int registerForCapabilitiesChanged( @@ -2419,7 +2715,10 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void unregisterForCapabilitiesChanged( @@ -2452,7 +2751,10 @@ public final class SatelliteManager { * * @return List of plmn for carrier satellite service. If no plmn is available, empty list will * be returned. + * + * @hide */ + @SystemApi @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) @NonNull public List<String> getSatellitePlmnsForCarrier(int subId) { diff --git a/tools/fonts/fontchain_linter.py b/tools/fonts/fontchain_linter.py index 006a02908643..3bde92909cd5 100755 --- a/tools/fonts/fontchain_linter.py +++ b/tools/fonts/fontchain_linter.py @@ -10,6 +10,14 @@ from xml.etree import ElementTree from fontTools import ttLib +# TODO(nona): Remove hard coded font version and unicode versions. +# Figure out a way of giving this information with command lines. +EMOJI_FONT_TO_UNICODE_MAP = { + '2.034': 15.0, + '2.042': 15.1, + '2.047': 16.0, +} + EMOJI_VS = 0xFE0F LANG_TO_SCRIPT = { @@ -217,9 +225,8 @@ def check_hyphens(hyphens_dir): class FontRecord(object): - def __init__(self, name, psName, scripts, variant, weight, style, fallback_for, font): + def __init__(self, name, scripts, variant, weight, style, fallback_for, font): self.name = name - self.psName = psName self.scripts = scripts self.variant = variant self.weight = weight @@ -282,13 +289,23 @@ def parse_fonts_xml(fonts_xml_path): m = trim_re.match(font_file) font_file = m.group(1) - weight = int(child.get('weight')) - assert weight % 100 == 0, ( - 'Font weight "%d" is not a multiple of 100.' % weight) - - style = child.get('style') - assert style in {'normal', 'italic'}, ( - 'Unknown style "%s"' % style) + # In case of variable font and it supports `wght` axis, the weight attribute can be + # dropped which is automatically adjusted at runtime. + if 'weight' in child: + weight = int(child.get('weight')) + assert weight % 100 == 0, ( + 'Font weight "%d" is not a multiple of 100.' % weight) + else: + weight = None + + # In case of variable font and it supports `ital` or `slnt` axes, the style attribute + # can be dropped which is automatically adjusted at runtime. + if 'style' in child: + style = child.get('style') + assert style in {'normal', 'italic'}, ( + 'Unknown style "%s"' % style) + else: + style = None fallback_for = child.get('fallbackFor') @@ -306,7 +323,6 @@ def parse_fonts_xml(fonts_xml_path): record = FontRecord( name, - child.get('postScriptName'), frozenset(scripts), variant, weight, @@ -357,6 +373,11 @@ def is_regional_indicator(x): # regional indicator A..Z return 0x1F1E6 <= x <= 0x1F1FF +def is_flag_sequence(seq): + if type(seq) == int: + return False + len(seq) == 2 and is_regional_indicator(seq[0]) and is_regional_indicator(seq[1]) + def is_tag(x): # tag block return 0xE0000 <= x <= 0xE007F @@ -391,17 +412,43 @@ def check_emoji_not_compat(all_emoji, equivalent_emoji): if "meta" in ttf: assert 'Emji' not in ttf["meta"].data, 'NotoColorEmoji MUST be a compat font' +def is_flag_emoji(font): + return 0x1F1E6 in get_best_cmap(font) + +def emoji_font_version_to_unicode_version(font_version): + version_str = '%.3f' % font_version + assert version_str in EMOJI_FONT_TO_UNICODE_MAP, 'Unknown emoji font verion: %s' % version_str + return EMOJI_FONT_TO_UNICODE_MAP[version_str] + def check_emoji_font_coverage(emoji_fonts, all_emoji, equivalent_emoji): coverages = [] + emoji_font_version = 0 + emoji_flag_font_version = 0 for emoji_font in emoji_fonts: coverages.append(get_emoji_map(emoji_font)) + # Find the largest version of the installed emoji font. + version = open_font(emoji_font)['head'].fontRevision + if is_flag_emoji(emoji_font): + emoji_flag_font_version = max(emoji_flag_font_version, version) + else: + emoji_font_version = max(emoji_font_version, version) + + emoji_flag_unicode_version = emoji_font_version_to_unicode_version(emoji_flag_font_version) + emoji_unicode_version = emoji_font_version_to_unicode_version(emoji_font_version) + errors = [] for sequence in all_emoji: if all([sequence not in coverage for coverage in coverages]): - errors.append('%s is not supported in the emoji font.' % printable(sequence)) + sequence_version = float(_age_by_chars[sequence]) + if is_flag_sequence(sequence): + if sequence_version <= emoji_flag_unicode_version: + errors.append('%s is not supported in the emoji font.' % printable(sequence)) + else: + if sequence_version <= emoji_unicode_version: + errors.append('%s is not supported in the emoji font.' % printable(sequence)) for coverage in coverages: for sequence in coverage: @@ -480,6 +527,19 @@ def check_emoji_defaults(default_emoji): repr(missing_text_chars)) +def parse_unicode_seq(chars): + if ' ' in chars: # character sequence + sequence = [int(ch, 16) for ch in chars.split(' ')] + additions = [tuple(sequence)] + elif '..' in chars: # character range + char_start, char_end = chars.split('..') + char_start = int(char_start, 16) + char_end = int(char_end, 16) + additions = range(char_start, char_end+1) + else: # single character + additions = [int(chars, 16)] + return additions + # Setting reverse to true returns a dictionary that maps the values to sets of # characters, useful for some binary properties. Otherwise, we get a # dictionary that maps characters to the property values, assuming there's only @@ -501,16 +561,8 @@ def parse_unicode_datafile(file_path, reverse=False): chars = chars.strip() prop = prop.strip() - if ' ' in chars: # character sequence - sequence = [int(ch, 16) for ch in chars.split(' ')] - additions = [tuple(sequence)] - elif '..' in chars: # character range - char_start, char_end = chars.split('..') - char_start = int(char_start, 16) - char_end = int(char_end, 16) - additions = range(char_start, char_end+1) - else: # singe character - additions = [int(chars, 16)] + additions = parse_unicode_seq(chars) + if reverse: output_dict[prop].update(additions) else: @@ -519,6 +571,32 @@ def parse_unicode_datafile(file_path, reverse=False): output_dict[addition] = prop return output_dict +def parse_sequence_age(file_path): + VERSION_RE = re.compile(r'E([\d\.]+)') + output_dict = {} + with open(file_path) as datafile: + for line in datafile: + comment = '' + if '#' in line: + hash_pos = line.index('#') + comment = line[hash_pos + 1:].strip() + line = line[:hash_pos] + line = line.strip() + if not line: + continue + + chars = line[:line.index(';')].strip() + + m = VERSION_RE.match(comment) + assert m, 'Version not found: unknown format: %s' % line + version = m.group(1) + + additions = parse_unicode_seq(chars) + + for addition in additions: + assert addition not in output_dict + output_dict[addition] = version + return output_dict def parse_emoji_variants(file_path): emoji_set = set() @@ -543,7 +621,7 @@ def parse_emoji_variants(file_path): def parse_ucd(ucd_path): - global _emoji_properties, _chars_by_age + global _emoji_properties, _chars_by_age, _age_by_chars global _text_variation_sequences, _emoji_variation_sequences global _emoji_sequences, _emoji_zwj_sequences _emoji_properties = parse_unicode_datafile( @@ -555,6 +633,10 @@ def parse_ucd(ucd_path): _chars_by_age = parse_unicode_datafile( path.join(ucd_path, 'DerivedAge.txt'), reverse=True) + _age_by_chars = parse_unicode_datafile( + path.join(ucd_path, 'DerivedAge.txt')) + _age_by_chars.update(parse_sequence_age( + path.join(ucd_path, 'emoji-sequences.txt'))) sequences = parse_emoji_variants( path.join(ucd_path, 'emoji-variation-sequences.txt')) _text_variation_sequences, _emoji_variation_sequences = sequences @@ -743,44 +825,12 @@ def check_cjk_punctuation(): break assert_font_supports_none_of_chars(record.font, cjk_punctuation, name) -def getPostScriptName(font): - font_file, index = font - font_path = path.join(_fonts_dir, font_file) - if index is not None: - # Use the first font file in the collection for resolving post script name. - ttf = ttLib.TTFont(font_path, fontNumber=0) - else: - ttf = ttLib.TTFont(font_path) - - nameTable = ttf['name'] - for name in nameTable.names: - if (name.nameID == 6 and name.platformID == 3 and name.platEncID == 1 - and name.langID == 0x0409): - return str(name) - -def check_canonical_name(): - for record in _all_fonts: - file_name, index = record.font - - psName = getPostScriptName(record.font) - if record.psName: - # If fonts element has postScriptName attribute, it should match with the PostScript - # name in the name table. - assert psName == record.psName, ('postScriptName attribute %s should match with %s' % ( - record.psName, psName)) - else: - # If fonts element doesn't have postScriptName attribute, the file name should match - # with the PostScript name in the name table. - assert psName == file_name[:-4], ('file name %s should match with %s' % ( - file_name, psName)) - - def main(): global _fonts_dir target_out = sys.argv[1] _fonts_dir = path.join(target_out, 'fonts') - fonts_xml_path = path.join(target_out, 'etc', 'fonts.xml') + fonts_xml_path = path.join(target_out, 'etc', 'font_fallback.xml') parse_fonts_xml(fonts_xml_path) @@ -793,8 +843,6 @@ def main(): check_cjk_punctuation() - check_canonical_name() - check_emoji = sys.argv[2] if check_emoji == 'true': ucd_path = sys.argv[3] |