diff options
168 files changed, 5451 insertions, 1601 deletions
diff --git a/apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java b/apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java index f65067fe2d92..afdc36190066 100644 --- a/apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java +++ b/apct-tests/perftests/autofill/src/android/view/autofill/AutofillTestWatcher.java @@ -174,7 +174,9 @@ final class AutofillTestWatcher extends TestWatcher { public static void onConnected() { Log.i(TAG, "onConnected: sServiceWatcher=" + sServiceWatcher); - + if (sServiceWatcher == null) { + sServiceWatcher = new ServiceWatcher(); + } sServiceWatcher.mConnected.countDown(); } diff --git a/cmds/screencap/Android.bp b/cmds/screencap/Android.bp index 16026eca2980..9f350b1d6054 100644 --- a/cmds/screencap/Android.bp +++ b/cmds/screencap/Android.bp @@ -7,25 +7,66 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -cc_binary { - name: "screencap", +cc_defaults { + name: "screencap_defaults", - srcs: ["screencap.cpp"], + cflags: [ + "-Wall", + "-Werror", + "-Wunreachable-code", + "-Wunused", + ], shared_libs: [ - "libcutils", - "libutils", "libbinder", - "libjnigraphics", + "libcutils", + "libgui", "libhwui", + "libjnigraphics", "libui", - "libgui", + "libutils", ], +} - cflags: [ - "-Wall", - "-Werror", - "-Wunused", - "-Wunreachable-code", +cc_library { + name: "libscreencap", + + defaults: [ + "screencap_defaults", + ], + + srcs: ["screencap_utils.cpp"], +} + +cc_binary { + name: "screencap", + + defaults: [ + "screencap_defaults", + ], + + srcs: ["screencap.cpp"], + + static_libs: [ + "libscreencap", + ], +} + +cc_test { + name: "libscreencap_test", + + defaults: [ + "screencap_defaults", + ], + + test_suites: ["device-tests"], + + srcs: [ + "tests/screencap_test.cpp", + ], + + static_libs: [ + "libgmock", + "libscreencap", ], } diff --git a/cmds/screencap/TEST_MAPPING b/cmds/screencap/TEST_MAPPING new file mode 100644 index 000000000000..05c598e1e9cc --- /dev/null +++ b/cmds/screencap/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "libscreencap_test" + } + ], + "hwasan-presubmit": [ + { + "name": "libscreencap_test" + } + ] +}
\ No newline at end of file diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp index d563ad3fd3db..9ff1161081fc 100644 --- a/cmds/screencap/screencap.cpp +++ b/cmds/screencap/screencap.cpp @@ -37,6 +37,9 @@ #include <ui/GraphicTypes.h> #include <ui/PixelFormat.h> +#include "utils/Errors.h" +#include "screencap_utils.h" + using namespace android; #define COLORSPACE_UNKNOWN 0 @@ -145,24 +148,6 @@ static status_t notifyMediaScanner(const char* fileName) { return NO_ERROR; } -status_t capture(const DisplayId displayId, - const gui::CaptureArgs& captureArgs, - ScreenCaptureResults& outResult) { - sp<SyncScreenCaptureListener> captureListener = new SyncScreenCaptureListener(); - ScreenshotClient::captureDisplay(displayId, captureArgs, captureListener); - - ScreenCaptureResults captureResults = captureListener->waitForResults(); - if (!captureResults.fenceResult.ok()) { - fprintf(stderr, "Failed to take screenshot. Status: %d\n", - fenceStatus(captureResults.fenceResult)); - return 1; - } - - outResult = captureResults; - - return 0; -} - status_t saveImage(const char* fn, std::optional<AndroidBitmapCompressFormat> format, const ScreenCaptureResults& captureResults) { void* base = nullptr; @@ -427,15 +412,12 @@ int main(int argc, char** argv) std::vector<ScreenCaptureResults> results; const size_t numDisplays = displaysToCapture.size(); - for (int i=0; i<numDisplays; i++) { - ScreenCaptureResults result; - + for (int i = 0; i < numDisplays; i++) { // 1. Capture the screen - if (const status_t captureStatus = - capture(displaysToCapture[i], captureArgs, result) != 0) { - - fprintf(stderr, "Capturing failed.\n"); - return captureStatus; + auto captureResult = screencap::capture(displaysToCapture[i], captureArgs); + if (!captureResult.ok()) { + fprintf(stderr, "%sCapturing failed.\n", captureResult.error().message().c_str()); + return 1; } // 2. Save the capture result as an image. @@ -453,7 +435,7 @@ int main(int argc, char** argv) if (!filename.empty()) { fn = filename.c_str(); } - if (const status_t saveImageStatus = saveImage(fn, format, result) != 0) { + if (const status_t saveImageStatus = saveImage(fn, format, captureResult.value()) != 0) { fprintf(stderr, "Saving image failed.\n"); return saveImageStatus; } diff --git a/cmds/screencap/screencap_utils.cpp b/cmds/screencap/screencap_utils.cpp new file mode 100644 index 000000000000..03ade73d0e30 --- /dev/null +++ b/cmds/screencap/screencap_utils.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "screencap_utils.h" + +#include "gui/SyncScreenCaptureListener.h" + +namespace android::screencap { + +base::Result<gui::ScreenCaptureResults> capture(const DisplayId displayId, + const gui::CaptureArgs& captureArgs) { + sp<SyncScreenCaptureListener> captureListener = new SyncScreenCaptureListener(); + auto captureDisplayStatus = + ScreenshotClient::captureDisplay(displayId, captureArgs, captureListener); + + gui::ScreenCaptureResults captureResults = captureListener->waitForResults(); + if (!captureResults.fenceResult.ok()) { + status_t captureStatus = fenceStatus(captureResults.fenceResult); + std::stringstream errorMsg; + errorMsg << "Failed to take take screenshot. "; + if (captureStatus == NAME_NOT_FOUND) { + errorMsg << "Display Id '" << displayId.value << "' is not valid.\n"; + } + return base::ResultError(errorMsg.str(), captureStatus); + } + + return captureResults; +} + +} // namespace android::screencap
\ No newline at end of file diff --git a/cmds/screencap/screencap_utils.h b/cmds/screencap/screencap_utils.h new file mode 100644 index 000000000000..6580e3fa5ff1 --- /dev/null +++ b/cmds/screencap/screencap_utils.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <android-base/result.h> +#include <android/gui/DisplayCaptureArgs.h> + +#include "gui/ScreenCaptureResults.h" +#include "ui/DisplayId.h" + +#pragma once + +namespace android::screencap { +base::Result<gui::ScreenCaptureResults> capture(const DisplayId displayId, + const gui::CaptureArgs& captureArgs); +} // namespace android::screencap diff --git a/cmds/screencap/tests/screencap_test.cpp b/cmds/screencap/tests/screencap_test.cpp new file mode 100644 index 000000000000..b7bfca9ada65 --- /dev/null +++ b/cmds/screencap/tests/screencap_test.cpp @@ -0,0 +1,68 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <binder/ProcessState.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <gui/SurfaceComposerClient.h> + +#include "android/gui/CaptureArgs.h" +#include "gmock/gmock.h" +#include "gui/ScreenCaptureResults.h" +#include "screencap_utils.h" +#include "ui/DisplayId.h" + +using ::android::DisplayId; +using ::android::OK; +using ::android::PhysicalDisplayId; +using ::android::ProcessState; +using ::android::SurfaceComposerClient; +using ::android::gui::CaptureArgs; +using ::android::gui::ScreenCaptureResults; +using ::testing::AllOf; +using ::testing::HasSubstr; + +class ScreenCapTest : public ::testing::Test { +protected: + static void SetUpTestSuite() { + // These lines are copied from screencap.cpp. They are necessary to call binder. + ProcessState::self()->setThreadPoolMaxThreadCount(0); + ProcessState::self()->startThreadPool(); + } +}; + +TEST_F(ScreenCapTest, Capture_InvalidDisplayNumber) { + DisplayId display; + display.value = -1; + + CaptureArgs args; + auto result = ::android::screencap::capture(display, args); + EXPECT_FALSE(result.ok()); + EXPECT_THAT(result.error().message(), + AllOf(HasSubstr("Display Id"), HasSubstr("is not valid."))); +} + +TEST_F(ScreenCapTest, Capture_SuccessWithPhysicalDisplay) { + const std::vector<PhysicalDisplayId> physicalDisplays = + SurfaceComposerClient::getPhysicalDisplayIds(); + + ASSERT_FALSE(physicalDisplays.empty()); + DisplayId display; + display.value = physicalDisplays.front().value; + + CaptureArgs args; + auto result = ::android::screencap::capture(display, args); + EXPECT_TRUE(result.ok()); + // TODO consider verifying actual captured image. +}
\ No newline at end of file diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ab824119d643..9a848d423c9a 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -16052,7 +16052,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isLteCdmaEvdoGsmWcdmaEnabled(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isMobileDataPolicyEnabled(int); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNrDualConnectivityEnabled(); - method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNullCipherNotificationsEnabled(); + method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNullCipherNotificationsEnabled(); method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isOffhook(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isOpportunisticNetworkEnabled(); method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isPotentialEmergencyNumber(@NonNull String); @@ -16097,7 +16097,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int setNrDualConnectivityState(int); - method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setNullCipherNotificationsEnabled(boolean); + method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setNullCipherNotificationsEnabled(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setOpportunisticNetworkState(boolean); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setPreferredNetworkTypeBitmask(long); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setRadio(boolean); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 0b0738ee14dc..c60a2d451ab3 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1722,6 +1722,7 @@ package android.hardware.display { public final class DisplayManager { method public boolean areUserDisabledHdrTypesAllowed(); method @RequiresPermission(android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE) public void clearGlobalUserPreferredDisplayMode(); + method @FlaggedApi("com.android.server.display.feature.flags.display_topology") @Nullable @RequiresPermission("android.permission.MANAGE_DISPLAYS") public android.hardware.display.DisplayTopology getDisplayTopology(); method @Nullable public android.view.Display.Mode getGlobalUserPreferredDisplayMode(); method @NonNull public android.hardware.display.HdrConversionMode getHdrConversionModeSetting(); method @NonNull public int[] getSupportedHdrOutputTypes(); @@ -1747,6 +1748,13 @@ package android.hardware.display { field public static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 64; // 0x40 } + @FlaggedApi("com.android.server.display.feature.flags.display_topology") public final class DisplayTopology implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.util.SparseArray<android.graphics.RectF> getAbsoluteBounds(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.display.DisplayTopology> CREATOR; + } + } package android.hardware.fingerprint { diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 4e6fb8d3a8e7..e6082d0df1f8 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -615,3 +615,10 @@ flag { bug: "346553745" is_exported: true } + +flag { + namespace: "multi_user" + name: "logout_user_api" + description: "Add API to logout user" + bug: "350045389" +} diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index a96de4b050a3..fded88212127 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -1873,6 +1873,8 @@ public final class DisplayManager { */ @RequiresPermission(MANAGE_DISPLAYS) @Nullable + @TestApi + @FlaggedApi(Flags.FLAG_DISPLAY_TOPOLOGY) public DisplayTopology getDisplayTopology() { return mGlobal.getDisplayTopology(); } diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java index 785a0e0adc48..4ed0fc056e7d 100644 --- a/core/java/android/hardware/display/DisplayTopology.java +++ b/core/java/android/hardware/display/DisplayTopology.java @@ -21,8 +21,10 @@ import static android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT; import static android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT; import static android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.Nullable; +import android.annotation.TestApi; import android.graphics.PointF; import android.graphics.RectF; import android.os.Parcel; @@ -39,6 +41,7 @@ import android.view.Display; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.feature.flags.Flags; import java.io.PrintWriter; import java.io.StringWriter; @@ -59,6 +62,8 @@ import java.util.Queue; * * @hide */ +@TestApi +@FlaggedApi(Flags.FLAG_DISPLAY_TOPOLOGY) public final class DisplayTopology implements Parcelable { private static final String TAG = "DisplayTopology"; private static final float EPSILON = 0.0001f; @@ -82,6 +87,7 @@ public final class DisplayTopology implements Parcelable { * @param px The value in logical pixels * @param dpi The logical density of the display * @return The value in density-independent pixels + * @hide */ public static float pxToDp(float px, int dpi) { return px * DisplayMetrics.DENSITY_DEFAULT / dpi; @@ -91,6 +97,7 @@ public final class DisplayTopology implements Parcelable { * @param dp The value in density-independent pixels * @param dpi The logical density of the display * @return The value in logical pixels + * @hide */ public static float dpToPx(float dp, int dpi) { return dp * dpi / DisplayMetrics.DENSITY_DEFAULT; @@ -108,8 +115,14 @@ public final class DisplayTopology implements Parcelable { */ private int mPrimaryDisplayId = Display.INVALID_DISPLAY; + /** + * @hide + */ public DisplayTopology() {} + /** + * @hide + */ public DisplayTopology(@Nullable TreeNode root, int primaryDisplayId) { mRoot = root; if (mRoot != null) { @@ -124,15 +137,24 @@ public final class DisplayTopology implements Parcelable { mPrimaryDisplayId = primaryDisplayId; } + /** + * @hide + */ public DisplayTopology(Parcel source) { this(source.readTypedObject(TreeNode.CREATOR), source.readInt()); } + /** + * @hide + */ @Nullable public TreeNode getRoot() { return mRoot; } + /** + * @hide + */ public int getPrimaryDisplayId() { return mPrimaryDisplayId; } @@ -144,6 +166,7 @@ public final class DisplayTopology implements Parcelable { * @param displayId The logical display ID * @param width The width of the display * @param height The height of the display + * @hide */ public void addDisplay(int displayId, float width, float height) { addDisplay(displayId, width, height, /* shouldLog= */ true); @@ -155,6 +178,7 @@ public final class DisplayTopology implements Parcelable { * @param width The new width * @param height The new height * @return True if the topology has changed. + * @hide */ public boolean updateDisplay(int displayId, float width, float height) { TreeNode display = findDisplay(displayId, mRoot); @@ -178,6 +202,7 @@ public final class DisplayTopology implements Parcelable { * one by one. * @param displayId The logical display ID * @return True if the display was present in the topology and removed. + * @hide */ public boolean removeDisplay(int displayId) { if (findDisplay(displayId, mRoot) == null) { @@ -221,6 +246,7 @@ public final class DisplayTopology implements Parcelable { * are the display IDs. * @throws IllegalArgumentException if the keys in {@code positions} are not the exact display * IDs in this topology, no more, no less + * @hide */ public void rearrange(Map<Integer, PointF> newPos) { if (mRoot == null) { @@ -346,6 +372,7 @@ public final class DisplayTopology implements Parcelable { /** * Clamp offsets and remove any overlaps between displays. + * @hide */ public void normalize() { if (mRoot == null) { @@ -494,6 +521,7 @@ public final class DisplayTopology implements Parcelable { /** * @return A deep copy of the topology that will not be modified by the system. + * @hide */ public DisplayTopology copy() { TreeNode rootCopy = mRoot == null ? null : mRoot.copy(); @@ -505,6 +533,7 @@ public final class DisplayTopology implements Parcelable { * (0, 0). * @return Map from logical display ID to the display's absolute bounds */ + @NonNull public SparseArray<RectF> getAbsoluteBounds() { Map<TreeNode, RectF> bounds = new HashMap<>(); getInfo(bounds, /* depths= */ null, /* parents= */ null, mRoot, /* x= */ 0, /* y= */ 0, @@ -529,6 +558,7 @@ public final class DisplayTopology implements Parcelable { /** * Print the object's state and debug information into the given stream. + * @hide * @param pw The stream to dump information to. */ public void dump(PrintWriter pw) { @@ -629,6 +659,9 @@ public final class DisplayTopology implements Parcelable { return result; } + /** + * @hide + */ @Nullable public static TreeNode findDisplay(int displayId, @Nullable TreeNode startingNode) { if (startingNode == null) { @@ -725,6 +758,7 @@ public final class DisplayTopology implements Parcelable { * @param densityPerDisplay The logical display densities, indexed by logical display ID * @return The graph representation of the topology. If there is a corner adjacency, the same * display will appear twice in the list of adjacent displays with both possible placements. + * @hide */ @Nullable public DisplayTopologyGraph getGraph(SparseIntArray densityPerDisplay) { @@ -839,6 +873,9 @@ public final class DisplayTopology implements Parcelable { } } + /** + * @hide + */ public static final class TreeNode implements Parcelable { public static final int POSITION_LEFT = 0; public static final int POSITION_TOP = 1; diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 18f9b2b9d74f..59bd9822c157 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -83,6 +83,8 @@ interface IUserManager { long getUserCreationTime(int userId); int getUserSwitchability(int userId); boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable, int mUserId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_USERS)") + int getUserLogoutability(int userId); boolean isRestricted(int userId); boolean canHaveRestrictedProfile(int userId); boolean canAddPrivateProfile(int userId); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index ce93c71ac776..c00f31db1a38 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2261,6 +2261,45 @@ public class UserManager { public @interface UserSwitchabilityResult {} /** + * Indicates that user can logout. + * @hide + */ + public static final int LOGOUTABILITY_STATUS_OK = 0; + + /** + * Indicates that user cannot logout because it is the system user. + * @hide + */ + public static final int LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER = 1; + + /** + * Indicates that user cannot logout because there is no suitable user to logout to. This is + * generally applicable to Headless System User Mode devices that do not have an interactive + * system user. + * @hide + */ + public static final int LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO = 2; + + /** + * Indicates that user cannot logout because user switch cannot happen. + * @hide + */ + public static final int LOGOUTABILITY_STATUS_CANNOT_SWITCH = 3; + + /** + * Result returned in {@link #getUserLogoutability()} indicating user logoutability. + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, prefix = { "LOGOUTABILITY_STATUS_" }, value = { + LOGOUTABILITY_STATUS_OK, + LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER, + LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO, + LOGOUTABILITY_STATUS_CANNOT_SWITCH + }) + public @interface UserLogoutability {} + + /** * A response code from {@link #removeUserWhenPossible(UserHandle, boolean)} indicating that * the specified user has been successfully removed. * @@ -2737,6 +2776,35 @@ public class UserManager { } /** + * Returns whether logging out is currently allowed for the context user. + * + * <p>Logging out is not allowed in the following cases: + * <ol> + * <li>the user is system user + * <li>there is no suitable user to logout to (if no interactive system user) + * <li>the user is in a phone call + * <li>{@link #DISALLOW_USER_SWITCH} is set + * <li>system user hasn't been unlocked yet + * </ol> + * + * @return A {@link UserLogoutability} flag indicating if the user can logout, + * one of {@link #LOGOUTABILITY_STATUS_OK}, + * {@link #LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER}, + * {@link #LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO}, + * {@link #LOGOUTABILITY_STATUS_CANNOT_SWITCH}. + * @hide + */ + @UserHandleAware + @RequiresPermission(Manifest.permission.MANAGE_USERS) + public @UserLogoutability int getUserLogoutability() { + try { + return mService.getUserLogoutability(mUserId); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Returns the userId for the context user. * * @return the userId of the context user. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 350906818cb4..900f22d2b37b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -9349,6 +9349,16 @@ public final class ViewRootImpl implements ViewParent, return mVibrator; } + /** + * Clears the system vibrator. + * + * <p>This method releases the reference to the system vibrator. It's crucial to call this + * method when the vibrator is no longer needed to prevent any potential memory leaks. + */ + public void clearSystemVibrator() { + mVibrator = null; + } + private @Nullable AutofillManager getAutofillManager() { if (mView instanceof ViewGroup) { ViewGroup decorView = (ViewGroup) mView; diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 68b5a261f507..1156503cf8e8 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -1130,6 +1130,19 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Adds a hierarchy op for app compat reachability. + * + * @param container The token for the container Task + * @param taskId The id of the current task + * @hide + */ + public WindowContainerTransaction setReachabilityOffset( + @NonNull WindowContainerToken container, int taskId, int x, int y) { + mHierarchyOps.add(HierarchyOp.createForReachability(container.asBinder(), taskId, x, y)); + return this; + } + + /** * Merges another WCT into this one. * @param transfer When true, this will transfer everything from other potentially leaving * other in an unusable state. When false, other is left alone, but @@ -1590,6 +1603,7 @@ public final class WindowContainerTransaction implements Parcelable { public static final int HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE = 22; public static final int HIERARCHY_OP_TYPE_SET_DISABLE_LAUNCH_ADJACENT = 23; public static final int HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK = 24; + public static final int HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY = 25; @IntDef(prefix = {"HIERARCHY_OP_TYPE_"}, value = { HIERARCHY_OP_TYPE_REPARENT, @@ -1617,6 +1631,7 @@ public final class WindowContainerTransaction implements Parcelable { HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE, HIERARCHY_OP_TYPE_SET_DISABLE_LAUNCH_ADJACENT, HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK, + HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY, }) @Retention(RetentionPolicy.SOURCE) public @interface HierarchyOpType { @@ -1630,6 +1645,10 @@ public final class WindowContainerTransaction implements Parcelable { public static final String LAUNCH_KEY_SHORTCUT_CALLING_PACKAGE = "android:transaction.hop.shortcut_calling_package"; + // The following keys are used to define the reachability direction after a double tap. + public static final String REACHABILITY_EVENT_X = "android:transaction.reachability_x"; + public static final String REACHABILITY_EVENT_Y = "android:transaction.reachability_y"; + @HierarchyOpType private final int mType; @@ -1665,6 +1684,9 @@ public final class WindowContainerTransaction implements Parcelable { private Bundle mLaunchOptions; @Nullable + private Bundle mAppCompatOptions; + + @Nullable private Intent mActivityIntent; /** Used as options for {@link #addTaskFragmentOperation}. */ @@ -1833,7 +1855,21 @@ public final class WindowContainerTransaction implements Parcelable { .build(); } - /** Creates a hierarchy op for setting a task non-trimmable by recents. */ + /** Create a hierarchy op for app compat reachability. */ + @NonNull + public static HierarchyOp createForReachability(IBinder container, int taskId, int x, + int y) { + final Bundle appCompatOptions = new Bundle(); + appCompatOptions.putInt(LAUNCH_KEY_TASK_ID, taskId); + appCompatOptions.putInt(REACHABILITY_EVENT_X, x); + appCompatOptions.putInt(REACHABILITY_EVENT_Y, y); + return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY) + .setAppCompatOptions(appCompatOptions) + .setContainer(container) + .build(); + } + + /** Create a hierarchy op for setting a task non-trimmable by recents. */ @NonNull @FlaggedApi(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) public static HierarchyOp createForSetTaskTrimmableFromRecents(@NonNull IBinder container, @@ -1863,6 +1899,7 @@ public final class WindowContainerTransaction implements Parcelable { mWindowingModes = copy.mWindowingModes; mActivityTypes = copy.mActivityTypes; mLaunchOptions = copy.mLaunchOptions; + mAppCompatOptions = copy.mAppCompatOptions; mActivityIntent = copy.mActivityIntent; mTaskFragmentOperation = copy.mTaskFragmentOperation; mKeyguardState = copy.mKeyguardState; @@ -1889,6 +1926,7 @@ public final class WindowContainerTransaction implements Parcelable { mWindowingModes = in.createIntArray(); mActivityTypes = in.createIntArray(); mLaunchOptions = in.readBundle(); + mAppCompatOptions = in.readBundle(getClass().getClassLoader()); mActivityIntent = in.readTypedObject(Intent.CREATOR); mTaskFragmentOperation = in.readTypedObject(TaskFragmentOperation.CREATOR); mKeyguardState = in.readTypedObject(KeyguardState.CREATOR); @@ -1966,6 +2004,11 @@ public final class WindowContainerTransaction implements Parcelable { } @Nullable + public Bundle getAppCompatOptions() { + return mAppCompatOptions; + } + + @Nullable public Intent getActivityIntent() { return mActivityIntent; } @@ -2100,6 +2143,9 @@ public final class WindowContainerTransaction implements Parcelable { case HIERARCHY_OP_TYPE_LAUNCH_TASK: sb.append(mLaunchOptions); break; + case HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY: + sb.append(mAppCompatOptions); + break; case HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT: sb.append("container=").append(mContainer).append(" clearRoot=").append(mToTop); break; @@ -2182,6 +2228,7 @@ public final class WindowContainerTransaction implements Parcelable { dest.writeIntArray(mWindowingModes); dest.writeIntArray(mActivityTypes); dest.writeBundle(mLaunchOptions); + dest.writeBundle(mAppCompatOptions); dest.writeTypedObject(mActivityIntent, flags); dest.writeTypedObject(mTaskFragmentOperation, flags); dest.writeTypedObject(mKeyguardState, flags); @@ -2245,6 +2292,9 @@ public final class WindowContainerTransaction implements Parcelable { private Bundle mLaunchOptions; @Nullable + private Bundle mAppCompatOptions; + + @Nullable private Intent mActivityIntent; @Nullable @@ -2328,6 +2378,11 @@ public final class WindowContainerTransaction implements Parcelable { return this; } + Builder setAppCompatOptions(@Nullable Bundle appCompatOptions) { + mAppCompatOptions = appCompatOptions; + return this; + } + Builder setActivityIntent(@Nullable Intent activityIntent) { mActivityIntent = activityIntent; return this; @@ -2407,6 +2462,7 @@ public final class WindowContainerTransaction implements Parcelable { hierarchyOp.mToTop = mToTop; hierarchyOp.mReparentTopOnly = mReparentTopOnly; hierarchyOp.mLaunchOptions = mLaunchOptions; + hierarchyOp.mAppCompatOptions = mAppCompatOptions; hierarchyOp.mActivityIntent = mActivityIntent; hierarchyOp.mPendingIntent = mPendingIntent; hierarchyOp.mAlwaysOnTop = mAlwaysOnTop; diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 9f6ea42c6fc4..25dc6723aa78 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -460,4 +460,15 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "clear_system_vibrator" + namespace: "windowing_frontend" + description: "Clears the system vibrator before attaching new window, to avoid leaks." + bug: "393190314" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index f49c5f1c2b0f..036faef7aa65 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -34,45 +34,38 @@ import android.os.Build; import android.os.Parcel; import android.os.ParcelFormatException; import android.os.Process; -import android.os.StatFs; import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.util.ArraySet; -import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collections; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.locks.ReentrantLock; /** * BatteryStatsHistory encapsulates battery history files. * Battery history record is appended into buffer {@link #mHistoryBuffer} and backed up into - * {@link #mActiveFile}. - * When {@link #mHistoryBuffer} size reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER}, + * {@link #mActiveFragment}. + * When {@link #mHistoryBuffer} size reaches {@link #mMaxHistoryBufferSize}, * current mActiveFile is closed and a new mActiveFile is open. * History files are under directory /data/system/battery-history/. - * History files have name battery-history-<num>.bin. The file number <num> starts from zero and - * grows sequentially. + * History files have name <num>.bf. The file number <num> corresponds to the + * monotonic time when the file was started. * The mActiveFile is always the highest numbered history file. * The lowest number file is always the oldest file. * The highest number file is always the newest file. - * The file number grows sequentially and we never skip number. - * When count of history files exceeds {@link BatteryStatsImpl.Constants#MAX_HISTORY_FILES}, + * The file number grows monotonically and we never skip number. + * When the total size of history files exceeds the maximum allowed value, * the lowest numbered file is deleted and a new file is open. * * All interfaces in BatteryStatsHistory should only be called by BatteryStatsImpl and protected by @@ -86,10 +79,6 @@ public class BatteryStatsHistory { // Current on-disk Parcel version. Must be updated when the format of the parcelable changes private static final int VERSION = 212; - private static final String HISTORY_DIR = "battery-history"; - private static final String FILE_SUFFIX = ".bh"; - private static final int MIN_FREE_SPACE = 100 * 1024 * 1024; - // Part of initial delta int that specifies the time delta. static final int DELTA_TIME_MASK = 0x7ffff; static final int DELTA_TIME_LONG = 0x7ffff; // The delta is a following long @@ -135,7 +124,7 @@ public class BatteryStatsHistory { // For state1, trace everything except the wakelock bit (which can race with // suspend) and the running bit (which isn't meaningful in traces). static final int STATE1_TRACE_MASK = ~(HistoryItem.STATE_WAKE_LOCK_FLAG - | HistoryItem.STATE_CPU_RUNNING_FLAG); + | HistoryItem.STATE_CPU_RUNNING_FLAG); // For state2, trace all bit changes. static final int STATE2_TRACE_MASK = ~0; @@ -146,22 +135,132 @@ public class BatteryStatsHistory { */ private static final int EXTRA_BUFFER_SIZE_WHEN_DIR_LOCKED = 100_000; + public abstract static class BatteryHistoryFragment + implements Comparable<BatteryHistoryFragment> { + public final long monotonicTimeMs; + + public BatteryHistoryFragment(long monotonicTimeMs) { + this.monotonicTimeMs = monotonicTimeMs; + } + + @Override + public int compareTo(BatteryHistoryFragment o) { + return Long.compare(monotonicTimeMs, o.monotonicTimeMs); + } + + @Override + public boolean equals(Object o) { + return monotonicTimeMs == ((BatteryHistoryFragment) o).monotonicTimeMs; + } + + @Override + public int hashCode() { + return Long.hashCode(monotonicTimeMs); + } + } + + /** + * Persistent storage for battery history fragments + */ + public interface BatteryHistoryStore { + /** + * Returns the table of contents, in the chronological order. + */ + List<BatteryHistoryFragment> getFragments(); + + /** + * Returns the earliest available fragment + */ + @Nullable + BatteryHistoryFragment getEarliestFragment(); + + /** + * Returns the latest available fragment + */ + @Nullable + BatteryHistoryFragment getLatestFragment(); + + /** + * Given a fragment, returns the earliest fragment that follows it whose monotonic + * start time falls within the specified range. `startTimeMs` is inclusive, `endTimeMs` + * is exclusive. + */ + @Nullable + BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs, + long endTimeMs); + + /** + * Acquires a lock on the entire store. + */ + void lock(); + + /** + * Acquires a lock unless the store is already locked by a different thread. Returns true + * if the lock has been successfully acquired. + */ + boolean tryLock(); + + /** + * Unlocks the store. + */ + void unlock(); + + /** + * Returns true if the store is currently locked. + */ + boolean isLocked(); + + /** + * Returns the total amount of storage occupied by history fragments, in bytes. + */ + int getSize(); + + /** + * Returns true if the store contains any history fragments, excluding the currently + * active partial fragment. + */ + boolean hasCompletedFragments(); + + /** + * Creates a new empty history fragment starting at the specified time. + */ + BatteryHistoryFragment createFragment(long monotonicStartTime); + + /** + * Writes a fragment to disk as raw bytes. + * + * @param fragmentComplete indicates if this fragment is done or still partial. + */ + void writeFragment(BatteryHistoryFragment fragment, @NonNull byte[] bytes, + boolean fragmentComplete); + + /** + * Reads a fragment as raw bytes. + */ + @Nullable + byte[] readFragment(BatteryHistoryFragment fragment); + + /** + * Removes all persistent fragments + */ + void reset(); + } + private final Parcel mHistoryBuffer; - private final File mSystemDir; private final HistoryStepDetailsCalculator mStepDetailsCalculator; private final Clock mClock; private int mMaxHistoryBufferSize; /** - * The active history file that the history buffer is backed up into. + * The active history fragment that the history buffer is backed up into. */ - private AtomicFile mActiveFile; + private BatteryHistoryFragment mActiveFragment; /** - * A list of history files with increasing timestamps. + * Persistent storage of history files. */ - private final BatteryHistoryDirectory mHistoryDir; + private final BatteryHistoryStore mStore; /** * A list of small history parcels, used when BatteryStatsImpl object is created from @@ -172,7 +271,7 @@ public class BatteryStatsHistory { /** * When iterating history files, the current file index. */ - private BatteryHistoryFile mCurrentFile; + private BatteryHistoryFragment mCurrentFragment; /** * When iterating history files, the current file parcel. @@ -221,326 +320,6 @@ public class BatteryStatsHistory { private int mIteratorCookie; private final BatteryStatsHistory mWritableHistory; - private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> { - public final long monotonicTimeMs; - public final AtomicFile atomicFile; - - private BatteryHistoryFile(File directory, long monotonicTimeMs) { - this.monotonicTimeMs = monotonicTimeMs; - atomicFile = new AtomicFile(new File(directory, monotonicTimeMs + FILE_SUFFIX)); - } - - @Override - public int compareTo(BatteryHistoryFile o) { - return Long.compare(monotonicTimeMs, o.monotonicTimeMs); - } - - @Override - public boolean equals(Object o) { - return monotonicTimeMs == ((BatteryHistoryFile) o).monotonicTimeMs; - } - - @Override - public int hashCode() { - return Long.hashCode(monotonicTimeMs); - } - - @Override - public String toString() { - return atomicFile.getBaseFile().toString(); - } - } - - private static class BatteryHistoryDirectory { - private final File mDirectory; - private final MonotonicClock mMonotonicClock; - private int mMaxHistorySize; - private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>(); - private final ReentrantLock mLock = new ReentrantLock(); - private boolean mCleanupNeeded; - - BatteryHistoryDirectory(File directory, MonotonicClock monotonicClock, int maxHistorySize) { - mDirectory = directory; - mMonotonicClock = monotonicClock; - mMaxHistorySize = maxHistorySize; - if (mMaxHistorySize == 0) { - Slog.w(TAG, "mMaxHistorySize should not be zero when writing history"); - } - } - - void setMaxHistorySize(int maxHistorySize) { - mMaxHistorySize = maxHistorySize; - cleanup(); - } - - void lock() { - mLock.lock(); - } - - boolean tryLock() { - return mLock.tryLock(); - } - - void unlock() { - mLock.unlock(); - if (mCleanupNeeded) { - cleanup(); - } - } - - boolean isLocked() { - return mLock.isLocked(); - } - - void load() { - Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); - mDirectory.mkdirs(); - if (!mDirectory.exists()) { - Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath()); - } - - final List<File> toRemove = new ArrayList<>(); - final Set<BatteryHistoryFile> dedup = new ArraySet<>(); - mDirectory.listFiles((dir, name) -> { - final int b = name.lastIndexOf(FILE_SUFFIX); - if (b <= 0) { - toRemove.add(new File(dir, name)); - return false; - } - try { - long monotonicTime = Long.parseLong(name.substring(0, b)); - dedup.add(new BatteryHistoryFile(mDirectory, monotonicTime)); - } catch (NumberFormatException e) { - toRemove.add(new File(dir, name)); - return false; - } - return true; - }); - if (!dedup.isEmpty()) { - mHistoryFiles.addAll(dedup); - Collections.sort(mHistoryFiles); - } - if (!toRemove.isEmpty()) { - // Clear out legacy history files, which did not follow the X-Y.bin naming format. - BackgroundThread.getHandler().post(() -> { - lock(); - try { - for (File file : toRemove) { - file.delete(); - } - } finally { - unlock(); - Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); - } - }); - } else { - Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); - } - } - - List<String> getFileNames() { - lock(); - try { - List<String> names = new ArrayList<>(); - for (BatteryHistoryFile historyFile : mHistoryFiles) { - names.add(historyFile.atomicFile.getBaseFile().getName()); - } - return names; - } finally { - unlock(); - } - } - - @Nullable - BatteryHistoryFile getFirstFile() { - lock(); - try { - if (!mHistoryFiles.isEmpty()) { - return mHistoryFiles.get(0); - } - return null; - } finally { - unlock(); - } - } - - @Nullable - BatteryHistoryFile getLastFile() { - lock(); - try { - if (!mHistoryFiles.isEmpty()) { - return mHistoryFiles.get(mHistoryFiles.size() - 1); - } - return null; - } finally { - unlock(); - } - } - - @Nullable - BatteryHistoryFile getNextFile(BatteryHistoryFile current, long startTimeMs, - long endTimeMs) { - if (!mLock.isHeldByCurrentThread()) { - throw new IllegalStateException("Iterating battery history without a lock"); - } - - int nextFileIndex = 0; - int firstFileIndex = 0; - // skip the last file because its data is in history buffer. - int lastFileIndex = mHistoryFiles.size() - 2; - for (int i = lastFileIndex; i >= 0; i--) { - BatteryHistoryFile file = mHistoryFiles.get(i); - if (current != null && file.monotonicTimeMs == current.monotonicTimeMs) { - nextFileIndex = i + 1; - } - if (file.monotonicTimeMs > endTimeMs) { - lastFileIndex = i - 1; - } - if (file.monotonicTimeMs <= startTimeMs) { - firstFileIndex = i; - break; - } - } - - if (nextFileIndex < firstFileIndex) { - nextFileIndex = firstFileIndex; - } - - if (nextFileIndex <= lastFileIndex) { - return mHistoryFiles.get(nextFileIndex); - } - - return null; - } - - BatteryHistoryFile makeBatteryHistoryFile() { - BatteryHistoryFile file = new BatteryHistoryFile(mDirectory, - mMonotonicClock.monotonicTime()); - lock(); - try { - mHistoryFiles.add(file); - } finally { - unlock(); - } - return file; - } - - void writeToParcel(Parcel out, boolean useBlobs, - long preferredEarliestIncludedTimestampMs) { - Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel"); - lock(); - try { - final long start = SystemClock.uptimeMillis(); - for (int i = 0; i < mHistoryFiles.size() - 1; i++) { - long monotonicEndTime = Long.MAX_VALUE; - if (i < mHistoryFiles.size() - 1) { - monotonicEndTime = mHistoryFiles.get(i + 1).monotonicTimeMs; - } - - if (monotonicEndTime < preferredEarliestIncludedTimestampMs) { - continue; - } - - AtomicFile file = mHistoryFiles.get(i).atomicFile; - byte[] raw = new byte[0]; - try { - raw = file.readFully(); - } catch (Exception e) { - Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); - } - - out.writeBoolean(true); - if (useBlobs) { - out.writeBlob(raw); - } else { - // Avoiding blobs in the check-in file for compatibility - out.writeByteArray(raw); - } - } - out.writeBoolean(false); - if (DEBUG) { - Slog.d(TAG, - "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start)); - } - } finally { - unlock(); - Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); - } - } - - int getFileCount() { - lock(); - try { - return mHistoryFiles.size(); - } finally { - unlock(); - } - } - - int getSize() { - lock(); - try { - int ret = 0; - for (int i = 0; i < mHistoryFiles.size() - 1; i++) { - ret += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); - } - return ret; - } finally { - unlock(); - } - } - - void reset() { - lock(); - try { - if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!"); - for (BatteryHistoryFile file : mHistoryFiles) { - file.atomicFile.delete(); - } - mHistoryFiles.clear(); - } finally { - unlock(); - } - } - - private void cleanup() { - Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup"); - try { - if (mDirectory == null) { - return; - } - - if (!tryLock()) { - mCleanupNeeded = true; - return; - } - - mCleanupNeeded = false; - try { - // if free disk space is less than 100MB, delete oldest history file. - if (!hasFreeDiskSpace(mDirectory)) { - BatteryHistoryFile oldest = mHistoryFiles.remove(0); - oldest.atomicFile.delete(); - } - - // if there is more history stored than allowed, delete oldest history files. - int size = getSize(); - while (size > mMaxHistorySize) { - BatteryHistoryFile oldest = mHistoryFiles.get(0); - int length = (int) oldest.atomicFile.getBaseFile().length(); - oldest.atomicFile.delete(); - mHistoryFiles.remove(0); - size -= length; - } - } finally { - unlock(); - } - } finally { - Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); - } - } - } - /** * A delegate responsible for computing additional details for a step in battery history. */ @@ -621,24 +400,22 @@ public class BatteryStatsHistory { /** * Constructor * - * @param systemDir typically /data/system - * @param maxHistorySize the largest amount of battery history to keep on disk * @param maxHistoryBufferSize the most amount of RAM to used for buffering of history steps */ - public BatteryStatsHistory(Parcel historyBuffer, File systemDir, - int maxHistorySize, int maxHistoryBufferSize, - HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock, - MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) { - this(historyBuffer, systemDir, maxHistorySize, maxHistoryBufferSize, stepDetailsCalculator, + public BatteryStatsHistory(Parcel historyBuffer, int maxHistoryBufferSize, + @Nullable BatteryHistoryStore store, HistoryStepDetailsCalculator stepDetailsCalculator, + Clock clock, MonotonicClock monotonicClock, TraceDelegate tracer, + EventLogger eventLogger) { + this(historyBuffer, maxHistoryBufferSize, store, + stepDetailsCalculator, clock, monotonicClock, tracer, eventLogger, null); } - private BatteryStatsHistory(@Nullable Parcel historyBuffer, @Nullable File systemDir, - int maxHistorySize, int maxHistoryBufferSize, + private BatteryStatsHistory(@Nullable Parcel historyBuffer, int maxHistoryBufferSize, + @Nullable BatteryHistoryStore store, @NonNull HistoryStepDetailsCalculator stepDetailsCalculator, @NonNull Clock clock, @NonNull MonotonicClock monotonicClock, @NonNull TraceDelegate tracer, @NonNull EventLogger eventLogger, @Nullable BatteryStatsHistory writableHistory) { - mSystemDir = systemDir; mMaxHistoryBufferSize = maxHistoryBufferSize; mStepDetailsCalculator = stepDetailsCalculator; mTracer = tracer; @@ -659,18 +436,16 @@ public class BatteryStatsHistory { } if (writableHistory != null) { - mHistoryDir = writableHistory.mHistoryDir; - } else if (systemDir != null) { - mHistoryDir = new BatteryHistoryDirectory(new File(systemDir, HISTORY_DIR), - monotonicClock, maxHistorySize); - mHistoryDir.load(); - BatteryHistoryFile activeFile = mHistoryDir.getLastFile(); - if (activeFile == null) { - activeFile = mHistoryDir.makeBatteryHistoryFile(); - } - setActiveFile(activeFile); + mStore = writableHistory.mStore; } else { - mHistoryDir = null; + mStore = store; + if (mStore != null) { + BatteryHistoryFragment activeFile = mStore.getLatestFragment(); + if (activeFile == null) { + activeFile = mStore.createFragment(mMonotonicClock.monotonicTime()); + } + setActiveFragment(activeFile); + } } } @@ -681,8 +456,7 @@ public class BatteryStatsHistory { private BatteryStatsHistory(Parcel parcel) { mClock = Clock.SYSTEM_CLOCK; mTracer = null; - mSystemDir = null; - mHistoryDir = null; + mStore = null; mStepDetailsCalculator = null; mEventLogger = new EventLogger(); mWritableHistory = null; @@ -718,15 +492,6 @@ public class BatteryStatsHistory { } /** - * Changes the maximum amount of history to be kept on disk. - */ - public void setMaxHistorySize(int maxHistorySize) { - if (mHistoryDir != null) { - mHistoryDir.setMaxHistorySize(maxHistorySize); - } - } - - /** * Changes the maximum size of the history buffer, in bytes. */ public void setMaxHistoryBufferSize(int maxHistoryBufferSize) { @@ -745,8 +510,8 @@ public class BatteryStatsHistory { Parcel historyBufferCopy = Parcel.obtain(); historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); - return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, - null, null, mEventLogger, this); + return new BatteryStatsHistory(historyBufferCopy, 0, mStore, null, + null, null, null, mEventLogger, this); } } finally { Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); @@ -757,45 +522,40 @@ public class BatteryStatsHistory { * Returns true if this instance only supports reading history. */ public boolean isReadOnly() { - return !mMutable || mActiveFile == null/* || mHistoryDir == null*/; + return !mMutable || mActiveFragment == null || mStore == null; } /** * Set the active file that mHistoryBuffer is backed up into. */ - private void setActiveFile(BatteryHistoryFile file) { - mActiveFile = file.atomicFile; + private void setActiveFragment(BatteryHistoryFragment file) { + mActiveFragment = file; if (DEBUG) { - Slog.d(TAG, "activeHistoryFile:" + mActiveFile.getBaseFile().getPath()); + Slog.d(TAG, "activeHistoryFile:" + mActiveFragment); } } /** - * When {@link #mHistoryBuffer} reaches {@link BatteryStatsImpl.Constants#MAX_HISTORY_BUFFER}, - * create next history file. + * When {@link #mHistoryBuffer} reaches {@link #mMaxHistoryBufferSize}, + * create next history fragment. */ - public void startNextFile(long elapsedRealtimeMs) { + public void startNextFragment(long elapsedRealtimeMs) { synchronized (this) { - startNextFileLocked(elapsedRealtimeMs); + startNextFragmentLocked(elapsedRealtimeMs); } } @GuardedBy("this") - private void startNextFileLocked(long elapsedRealtimeMs) { + private void startNextFragmentLocked(long elapsedRealtimeMs) { final long start = SystemClock.uptimeMillis(); - writeHistory(); + writeHistory(true /* fragmentComplete */); if (DEBUG) { Slog.d(TAG, "writeHistory took ms:" + (SystemClock.uptimeMillis() - start)); } - setActiveFile(mHistoryDir.makeBatteryHistoryFile()); - try { - mActiveFile.getBaseFile().createNewFile(); - } catch (IOException e) { - Slog.e(TAG, "Could not create history file: " + mActiveFile.getBaseFile()); - } - - mHistoryBufferStartTime = mMonotonicClock.monotonicTime(elapsedRealtimeMs); + long monotonicStartTime = mMonotonicClock.monotonicTime(elapsedRealtimeMs); + setActiveFragment(mStore.createFragment(monotonicStartTime)); + mHistoryBufferStartTime = monotonicStartTime; mHistoryBuffer.setDataSize(0); mHistoryBuffer.setDataPosition(0); mHistoryBuffer.setDataCapacity(mMaxHistoryBufferSize / 2); @@ -810,7 +570,6 @@ public class BatteryStatsHistory { } mWrittenPowerStatsDescriptors.clear(); - mHistoryDir.cleanup(); } /** @@ -818,7 +577,7 @@ public class BatteryStatsHistory { * currently being read. */ public boolean isResetEnabled() { - return mHistoryDir == null || !mHistoryDir.isLocked(); + return mStore == null || !mStore.isLocked(); } /** @@ -827,11 +586,11 @@ public class BatteryStatsHistory { */ public void reset() { synchronized (this) { - if (mHistoryDir != null) { - mHistoryDir.reset(); - setActiveFile(mHistoryDir.makeBatteryHistoryFile()); - } initHistoryBuffer(); + if (mStore != null) { + mStore.reset(); + setActiveFragment(mStore.createFragment(mHistoryBufferStartTime)); + } } } @@ -840,9 +599,9 @@ public class BatteryStatsHistory { */ public long getStartTime() { synchronized (this) { - BatteryHistoryFile file = mHistoryDir.getFirstFile(); - if (file != null) { - return file.monotonicTimeMs; + BatteryHistoryFragment firstFragment = mStore.getEarliestFragment(); + if (firstFragment != null) { + return firstFragment.monotonicTimeMs; } else { return mHistoryBufferStartTime; } @@ -863,10 +622,10 @@ public class BatteryStatsHistory { return copy().iterate(startTimeMs, endTimeMs); } - if (mHistoryDir != null) { - mHistoryDir.lock(); + if (mStore != null) { + mStore.lock(); } - mCurrentFile = null; + mCurrentFragment = null; mCurrentParcel = null; mCurrentParcelEnd = 0; mParcelIndex = 0; @@ -883,8 +642,8 @@ public class BatteryStatsHistory { */ void iteratorFinished() { mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); - if (mHistoryDir != null) { - mHistoryDir.unlock(); + if (mStore != null) { + mStore.unlock(); } Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate", mIteratorCookie); @@ -918,27 +677,26 @@ public class BatteryStatsHistory { } } - if (mHistoryDir != null) { - BatteryHistoryFile nextFile = mHistoryDir.getNextFile(mCurrentFile, startTimeMs, + if (mStore != null) { + BatteryHistoryFragment next = mStore.getNextFragment(mCurrentFragment, startTimeMs, endTimeMs); - while (nextFile != null) { + while (next != null) { mCurrentParcel = null; mCurrentParcelEnd = 0; final Parcel p = Parcel.obtain(); - AtomicFile file = nextFile.atomicFile; - if (readFileToParcel(p, file)) { + if (readFragmentToParcel(p, next)) { int bufSize = p.readInt(); int curPos = p.dataPosition(); mCurrentParcelEnd = curPos + bufSize; mCurrentParcel = p; if (curPos < mCurrentParcelEnd) { - mCurrentFile = nextFile; + mCurrentFragment = next; return mCurrentParcel; } } else { p.recycle(); } - nextFile = mHistoryDir.getNextFile(nextFile, startTimeMs, endTimeMs); + next = mStore.getNextFragment(next, startTimeMs, endTimeMs); } } @@ -988,39 +746,26 @@ public class BatteryStatsHistory { * Read history file into a parcel. * * @param out the Parcel read into. - * @param file the File to read from. + * @param fragment the fragment to read from. * @return true if success, false otherwise. */ - public boolean readFileToParcel(Parcel out, AtomicFile file) { - Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read"); - try { - byte[] raw = null; - try { - final long start = SystemClock.uptimeMillis(); - raw = file.readFully(); - if (DEBUG) { - Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() - + " duration ms:" + (SystemClock.uptimeMillis() - start)); - } - } catch (Exception e) { - Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); - return false; - } - out.unmarshall(raw, 0, raw.length); - out.setDataPosition(0); - if (!verifyVersion(out)) { - return false; - } - // skip monotonic time field. - out.readLong(); - // skip monotonic end time field - out.readLong(); - // skip monotonic size field - out.readLong(); - return true; - } finally { - Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + public boolean readFragmentToParcel(Parcel out, BatteryHistoryFragment fragment) { + byte[] data = mStore.readFragment(fragment); + if (data == null) { + return false; + } + out.unmarshall(data, 0, data.length); + out.setDataPosition(0); + if (!verifyVersion(out)) { + return false; } + // skip monotonic time field. + out.readLong(); + // skip monotonic end time field + out.readLong(); + // skip monotonic size field + out.readLong(); + return true; } /** @@ -1106,9 +851,8 @@ public class BatteryStatsHistory { public void writeToParcel(Parcel out) { synchronized (this) { writeHistoryBuffer(out); - /* useBlobs */ - if (mHistoryDir != null) { - mHistoryDir.writeToParcel(out, false /* useBlobs */, 0); + if (mStore != null) { + writeToParcel(out, false /* useBlobs */, 0); } } } @@ -1122,13 +866,54 @@ public class BatteryStatsHistory { public void writeToBatteryUsageStatsParcel(Parcel out, long preferredHistoryDurationMs) { synchronized (this) { out.writeBlob(mHistoryBuffer.marshall()); - if (mHistoryDir != null) { - mHistoryDir.writeToParcel(out, true /* useBlobs */, + if (mStore != null) { + writeToParcel(out, true /* useBlobs */, mHistoryMonotonicEndTime - preferredHistoryDurationMs); } } } + private void writeToParcel(Parcel out, boolean useBlobs, + long preferredEarliestIncludedTimestampMs) { + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel"); + mStore.lock(); + try { + final long start = SystemClock.uptimeMillis(); + List<BatteryHistoryFragment> fragments = mStore.getFragments(); + for (int i = 0; i < fragments.size() - 1; i++) { + long monotonicEndTime = Long.MAX_VALUE; + if (i < fragments.size() - 1) { + monotonicEndTime = fragments.get(i + 1).monotonicTimeMs; + } + + if (monotonicEndTime < preferredEarliestIncludedTimestampMs) { + continue; + } + + byte[] data = mStore.readFragment(fragments.get(i)); + if (data == null) { + Slog.e(TAG, "Error reading history fragment " + fragments.get(i)); + continue; + } + + out.writeBoolean(true); + if (useBlobs) { + out.writeBlob(data, 0, data.length); + } else { + // Avoiding blobs in the check-in file for compatibility + out.writeByteArray(data, 0, data.length); + } + } + out.writeBoolean(false); + if (DEBUG) { + Slog.d(TAG, "writeToParcel duration ms:" + (SystemClock.uptimeMillis() - start)); + } + } finally { + mStore.unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + /** * Reads a BatteryStatsHistory from a parcel written with * the {@link #writeToBatteryUsageStatsParcel} method. @@ -1141,28 +926,21 @@ public class BatteryStatsHistory { * Read history from a check-in file. */ public boolean readSummary() { - if (mActiveFile == null) { + if (mActiveFragment == null) { Slog.w(TAG, "readSummary: no history file associated with this instance"); return false; } Parcel parcel = Parcel.obtain(); try { - final long start = SystemClock.uptimeMillis(); - if (mActiveFile.exists()) { - byte[] raw = mActiveFile.readFully(); - if (raw.length > 0) { - parcel.unmarshall(raw, 0, raw.length); - parcel.setDataPosition(0); - readHistoryBuffer(parcel); - } - if (DEBUG) { - Slog.d(TAG, "read history file::" - + mActiveFile.getBaseFile().getPath() - + " bytes:" + raw.length + " took ms:" + (SystemClock.uptimeMillis() - - start)); - } + byte[] data = mStore.readFragment(mActiveFragment); + if (data == null) { + return false; } + + parcel.unmarshall(data, 0, data.length); + parcel.setDataPosition(0); + readHistoryBuffer(parcel); } catch (Exception e) { Slog.e(TAG, "Error reading battery history", e); reset(); @@ -1201,41 +979,21 @@ public class BatteryStatsHistory { } } - /** - * @return true if there is more than 100MB free disk space left. - */ - @android.ravenwood.annotation.RavenwoodReplace - private static boolean hasFreeDiskSpace(File systemDir) { - final StatFs stats = new StatFs(systemDir.getAbsolutePath()); - return stats.getAvailableBytes() > MIN_FREE_SPACE; - } - - private static boolean hasFreeDiskSpace$ravenwood(File systemDir) { - return true; - } - @VisibleForTesting - public List<String> getFilesNames() { - return mHistoryDir.getFileNames(); + public BatteryHistoryStore getBatteryHistoryStore() { + return mStore; } @VisibleForTesting - public AtomicFile getActiveFile() { - return mActiveFile; - } - - /** - * Returns the maximum storage size allocated to battery history. - */ - public int getMaxHistorySize() { - return mHistoryDir.mMaxHistorySize; + public BatteryHistoryFragment getActiveFragment() { + return mActiveFragment; } /** * @return the total size of all history files and history buffer. */ public int getHistoryUsedSize() { - int ret = mHistoryDir.getSize(); + int ret = mStore.getSize(); ret += mHistoryBuffer.dataSize(); if (mHistoryParcels != null) { for (int i = 0; i < mHistoryParcels.size(); i++) { @@ -1293,7 +1051,7 @@ public class BatteryStatsHistory { */ public void continueRecordingHistory() { synchronized (this) { - if (mHistoryBuffer.dataPosition() <= 0 && mHistoryDir.getFileCount() <= 1) { + if (mHistoryBuffer.dataPosition() <= 0 && !mStore.hasCompletedFragments()) { return; } @@ -1852,7 +1610,7 @@ public class BatteryStatsHistory { } final long timeDiffMs = mMonotonicClock.monotonicTime(elapsedRealtimeMs) - - mHistoryLastWritten.time; + - mHistoryLastWritten.time; final int diffStates = mHistoryLastWritten.states ^ cur.states; final int diffStates2 = mHistoryLastWritten.states2 ^ cur.states2; final int lastDiffStates = mHistoryLastWritten.states ^ mHistoryLastLastWritten.states; @@ -1953,7 +1711,7 @@ public class BatteryStatsHistory { mMaxHistoryBufferSize = 1024; } - boolean successfullyLocked = mHistoryDir.tryLock(); + boolean successfullyLocked = mStore.tryLock(); if (!successfullyLocked) { // Already locked by another thread // If the buffer size is below the allowed overflow limit, just keep going if (dataSize < mMaxHistoryBufferSize + EXTRA_BUFFER_SIZE_WHEN_DIR_LOCKED) { @@ -1971,10 +1729,10 @@ public class BatteryStatsHistory { copy.setTo(cur); try { - startNextFile(elapsedRealtimeMs); + startNextFragment(elapsedRealtimeMs); } finally { if (successfullyLocked) { - mHistoryDir.unlock(); + mStore.unlock(); } } @@ -2095,6 +1853,7 @@ public class BatteryStatsHistory { Battery charge int: if F in the first token is set, an int representing the battery charge in coulombs follows. */ + /** * Writes the delta between the previous and current history items into history buffer. */ @@ -2376,9 +2135,13 @@ public class BatteryStatsHistory { } /** - * Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} . + * Saves the accumulated history buffer in the active file, see {@link #getActiveFragment()} . */ public void writeHistory() { + writeHistory(false /* fragmentComplete */); + } + + private void writeHistory(boolean fragmentComplete) { synchronized (this) { if (isReadOnly()) { Slog.w(TAG, "writeHistory: this instance instance is read-only"); @@ -2397,7 +2160,7 @@ public class BatteryStatsHistory { Slog.d(TAG, "writeHistoryBuffer duration ms:" + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize()); } - writeParcelToFileLocked(p, mActiveFile); + writeParcelLocked(p, mActiveFragment, fragmentComplete); } finally { p.recycle(); } @@ -2457,30 +2220,18 @@ public class BatteryStatsHistory { } @GuardedBy("this") - private void writeParcelToFileLocked(Parcel p, AtomicFile file) { - FileOutputStream fos = null; + private void writeParcelLocked(Parcel p, BatteryHistoryFragment fragment, + boolean fragmentComplete) { mWriteLock.lock(); try { final long startTimeMs = SystemClock.uptimeMillis(); - fos = file.startWrite(); - fos.write(p.marshall()); - fos.flush(); - file.finishWrite(fos); - if (DEBUG) { - Slog.d(TAG, "writeParcelToFileLocked file:" + file.getBaseFile().getPath() - + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs) - + " bytes:" + p.dataSize()); - } + mStore.writeFragment(fragment, p.marshall(), fragmentComplete); mEventLogger.writeCommitSysConfigFile(startTimeMs); - } catch (IOException e) { - Slog.w(TAG, "Error writing battery statistics", e); - file.failWrite(fos); } finally { mWriteLock.unlock(); } } - /** * Returns the total number of history tags in the tag pool. */ diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index 90f8a0714a04..d73e2d47348b 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -436,6 +436,9 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { if (viewRoot != null) { // Clear the old callbacks and attach to the new window. viewRoot.getOnBackInvokedDispatcher().clear(); + if (Flags.clearSystemVibrator()) { + viewRoot.clearSystemVibrator(); + } onViewRootImplSet(viewRoot); } } diff --git a/core/jni/android_view_SurfaceControlActivePictureListener.cpp b/core/jni/android_view_SurfaceControlActivePictureListener.cpp index 15132db2a569..ee8efe19e15e 100644 --- a/core/jni/android_view_SurfaceControlActivePictureListener.cpp +++ b/core/jni/android_view_SurfaceControlActivePictureListener.cpp @@ -106,11 +106,13 @@ struct SurfaceControlActivePictureListener : public gui::BnActivePictureListener } status_t startListening() { - return SurfaceComposerClient::addActivePictureListener(this); + return SurfaceComposerClient::addActivePictureListener( + sp<SurfaceControlActivePictureListener>::fromExisting(this)); } status_t stopListening() { - return SurfaceComposerClient::removeActivePictureListener(this); + return SurfaceComposerClient::removeActivePictureListener( + sp<SurfaceControlActivePictureListener>::fromExisting(this)); } protected: diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 3edc5c108083..aaf84201821f 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1862,9 +1862,38 @@ <enum name="sync" value="2" /> </attr> - <!-- This attribute will be used to override app compatibility mode on 16 KB devices. - If set to enabled, Natives lib will be extracted from APK if they are not page aligned on - 16 KB device. 4 KB natives libs will be loaded app-compat mode if they are eligible. + <!-- This attribute overrides the user-set or platform-set 16 KB page size + compatibility mode, so that page agnostic compatibility is always enabled + or always disabled, rather than according to the user's preference. + + <p>On 4 KB systems, this attribute is ignored and apps are installed + normally. + + <p>On 16 KB systems, if an app is built for 16 KB page sizes, this + attribute is ignored and apps are installed normally. + + <p>This attribute only affects 16 KB systems for apps that are built + with 4 KB page size (old) options. + + <p>When page agnostic compatibility is enabled (either through this + flag or via the user's preference), the system specializes the app + installation process in ways known to improve compatibility of 4 KB + built apps on 16 KB systems. That is, apps which do not have aligned + libraries in APK files are extracted, requiring more space on the + device. An additional specialization when this option is enabled is + that the linker loads the application in a special mode intended + to allow 4 KB aligned program segments to load on a 16 KB page system. + + <p>Here are the situations where this attribute should be most useful: + <ul> + <li>If an app works on 16 KB mode, but is not built for it, enabling this + attribute forces the app to be installed in 16 KB mode without + the user having to set these options themself. + <li>If an app is fully working in 16 KB mode, you can set this + attribute to disabled, so that any regression causes a clear failure + and this compatibility mode is not used. + </ul> + @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) --> <attr name="pageSizeCompat"> <!-- value for enabled must match with diff --git a/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml new file mode 100644 index 000000000000..c2a20b977b70 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16" + android:tint="?android:attr/textColorSecondary"> + <path + android:fillColor="#FF000000" + android:pathData="M 8 11.375 L 2 5.375 L 3.4 3.975 L 8 8.575 L 12.6 3.975 L 14 5.375 L 8 11.375 Z" + /> +</vector> + diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml index 87c520ca1b51..b898e4b06c14 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml @@ -64,7 +64,7 @@ android:id="@+id/expand_menu_button" android:layout_width="16dp" android:layout_height="16dp" - android:src="@drawable/ic_baseline_expand_more_24" + android:src="@drawable/ic_baseline_expand_more_16" android:background="@null" android:scaleType="fitCenter" android:clickable="false" @@ -101,7 +101,7 @@ android:layout_width="44dp" android:layout_height="40dp" android:layout_gravity="end" - android:layout_marginHorizontal="8dp" + android:layout_marginEnd="8dp" android:clickable="true" android:focusable="true"/> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 404bbd1d0a33..e23d5725e9c3 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -295,6 +295,10 @@ <dimen name="bubble_bar_dismiss_zone_width">192dp</dimen> <!-- Height of the box around bottom center of the screen where drag only leads to dismiss --> <dimen name="bubble_bar_dismiss_zone_height">242dp</dimen> + <!-- Height of the box at the corner of the screen where drag leads to app moving to bubble --> + <dimen name="bubble_transform_area_width">140dp</dimen> + <!-- Width of the box at the corner of the screen where drag leads to app moving to bubble --> + <dimen name="bubble_transform_area_height">140dp</dimen> <!-- Bottom and end margin for compat buttons. --> <dimen name="compat_button_margin">24dp</dimen> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt new file mode 100644 index 000000000000..9bee11a92430 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.graphics.Rect + +/** + * Provide bounds for Bubbles drop targets that are shown when dragging over drag zones + */ +interface BubbleDropTargetBoundsProvider { + /** + * Get bubble bar expanded view visual drop target bounds on screen + */ + fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt new file mode 100644 index 000000000000..5d346c047123 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.graphics.Rect + +/** + * Represents an invisible area on the screen that determines what happens to a dragged object if it + * is released in that area. + * + * [bounds] are the bounds of the drag zone. Drag zones have an associated drop target that serves + * as visual feedback hinting what would happen if the object is released. When a dragged object is + * dragged into a drag zone, the associated drop target will be displayed. Not all drag zones have + * drop targets; only those that are made visible by Bubbles do. + */ +sealed interface DragZone { + + /** The bounds of this drag zone. */ + val bounds: Rect + + fun contains(x: Int, y: Int) = bounds.contains(x, y) + + /** Represents the bubble drag area on the screen. */ + sealed class Bubble(override val bounds: Rect) : DragZone { + data class Left(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds) + data class Right(override val bounds: Rect, val dropTarget: Rect) : Bubble(bounds) + } + + /** Represents dragging to Desktop Window. */ + data class DesktopWindow(override val bounds: Rect, val dropTarget: Rect) : DragZone + + /** Represents dragging to Full Screen. */ + data class FullScreen(override val bounds: Rect, val dropTarget: Rect) : DragZone + + /** Represents dragging to dismiss. */ + data class Dismiss(override val bounds: Rect) : DragZone + + /** Represents dragging to enter Split or replace a Split app. */ + sealed class Split(override val bounds: Rect) : DragZone { + data class Left(override val bounds: Rect) : Split(bounds) + data class Right(override val bounds: Rect) : Split(bounds) + data class Top(override val bounds: Rect) : Split(bounds) + data class Bottom(override val bounds: Rect) : Split(bounds) + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt new file mode 100644 index 000000000000..c2eef33881be --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.graphics.Rect +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode + +/** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */ +class DragZoneFactory( + private val deviceConfig: DeviceConfig, + private val splitScreenModeChecker: SplitScreenModeChecker, + private val desktopWindowModeChecker: DesktopWindowModeChecker, +) { + + private val windowBounds: Rect + get() = deviceConfig.windowBounds + + // TODO b/393172431: move these to xml + private val dismissDragZoneSize = if (deviceConfig.isSmallTablet) 140 else 200 + private val bubbleDragZoneTabletSize = 200 + private val bubbleDragZoneFoldableSize = 140 + private val fullScreenDragZoneWidth = 512 + private val fullScreenDragZoneHeight = 44 + private val desktopWindowDragZoneWidth = 880 + private val desktopWindowDragZoneHeight = 300 + private val desktopWindowFromExpandedViewDragZoneWidth = 200 + private val desktopWindowFromExpandedViewDragZoneHeight = 350 + private val splitFromBubbleDragZoneHeight = 100 + private val splitFromBubbleDragZoneWidth = 60 + private val hSplitFromExpandedViewDragZoneWidth = 60 + private val vSplitFromExpandedViewDragZoneWidth = 200 + private val vSplitFromExpandedViewDragZoneHeightTablet = 285 + private val vSplitFromExpandedViewDragZoneHeightFold = 150 + private val vUnevenSplitFromExpandedViewDragZoneHeight = 96 + + /** + * Creates the list of drag zones for the dragged object. + * + * Drag zones may have overlap, but the list is sorted by priority where the first drag zone has + * the highest priority so it should be checked first. + */ + fun createSortedDragZones(draggedObject: DraggedObject): List<DragZone> { + val dragZones = mutableListOf<DragZone>() + when (draggedObject) { + is DraggedObject.BubbleBar -> { + dragZones.add(createDismissDragZone()) + dragZones.addAll(createBubbleDragZones()) + } + is DraggedObject.Bubble -> { + dragZones.add(createDismissDragZone()) + dragZones.addAll(createBubbleDragZones()) + dragZones.add(createFullScreenDragZone()) + if (shouldShowDesktopWindowDragZones()) { + dragZones.add(createDesktopWindowDragZoneForBubble()) + } + dragZones.addAll(createSplitScreenDragZonesForBubble()) + } + is DraggedObject.ExpandedView -> { + dragZones.add(createDismissDragZone()) + dragZones.add(createFullScreenDragZone()) + if (shouldShowDesktopWindowDragZones()) { + dragZones.add(createDesktopWindowDragZoneForExpandedView()) + } + if (deviceConfig.isSmallTablet) { + dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnFoldable()) + } else { + dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet()) + } + createBubbleDragZonesForExpandedView() + } + } + return dragZones + } + + private fun createDismissDragZone(): DragZone { + return DragZone.Dismiss( + bounds = + Rect( + windowBounds.right / 2 - dismissDragZoneSize / 2, + windowBounds.bottom - dismissDragZoneSize, + windowBounds.right / 2 + dismissDragZoneSize / 2, + windowBounds.bottom + ) + ) + } + + private fun createBubbleDragZones(): List<DragZone> { + val dragZoneSize = + if (deviceConfig.isSmallTablet) { + bubbleDragZoneFoldableSize + } else { + bubbleDragZoneTabletSize + } + return listOf( + DragZone.Bubble.Left( + bounds = + Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom), + dropTarget = Rect(0, 0, 0, 0), + ), + DragZone.Bubble.Right( + bounds = + Rect( + windowBounds.right - dragZoneSize, + windowBounds.bottom - dragZoneSize, + windowBounds.right, + windowBounds.bottom, + ), + dropTarget = Rect(0, 0, 0, 0), + ) + ) + } + + private fun createBubbleDragZonesForExpandedView(): List<DragZone> { + return listOf( + DragZone.Bubble.Left( + bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom), + dropTarget = Rect(0, 0, 0, 0), + ), + DragZone.Bubble.Right( + bounds = + Rect( + windowBounds.right / 2, + 0, + windowBounds.right, + windowBounds.bottom, + ), + dropTarget = Rect(0, 0, 0, 0), + ) + ) + } + + private fun createFullScreenDragZone(): DragZone { + return DragZone.FullScreen( + bounds = + Rect( + windowBounds.right / 2 - fullScreenDragZoneWidth / 2, + 0, + windowBounds.right / 2 + fullScreenDragZoneWidth / 2, + fullScreenDragZoneHeight + ), + dropTarget = Rect(0, 0, 0, 0) + ) + } + + private fun shouldShowDesktopWindowDragZones() = + !deviceConfig.isSmallTablet && desktopWindowModeChecker.isSupported() + + private fun createDesktopWindowDragZoneForBubble(): DragZone { + return DragZone.DesktopWindow( + bounds = + if (deviceConfig.isLandscape) { + Rect( + windowBounds.right / 2 - desktopWindowDragZoneWidth / 2, + windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2, + windowBounds.right / 2 + desktopWindowDragZoneWidth / 2, + windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2 + ) + } else { + Rect( + 0, + windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2, + windowBounds.right, + windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2 + ) + }, + dropTarget = Rect(0, 0, 0, 0) + ) + } + + private fun createDesktopWindowDragZoneForExpandedView(): DragZone { + return DragZone.DesktopWindow( + bounds = + Rect( + windowBounds.right / 2 - desktopWindowFromExpandedViewDragZoneWidth / 2, + windowBounds.bottom / 2 - desktopWindowFromExpandedViewDragZoneHeight / 2, + windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2, + windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2 + ), + dropTarget = Rect(0, 0, 0, 0) + ) + } + + private fun createSplitScreenDragZonesForBubble(): List<DragZone> { + // for foldables in landscape mode or tables in portrait modes we have vertical split drag + // zones. otherwise we have horizontal split drag zones. + val isVerticalSplit = deviceConfig.isSmallTablet == deviceConfig.isLandscape + return if (isVerticalSplit) { + when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.SPLIT_50_50, + SplitScreenMode.NONE -> + listOf( + DragZone.Split.Top( + bounds = Rect(0, 0, windowBounds.right, windowBounds.bottom / 2), + ), + DragZone.Split.Bottom( + bounds = + Rect( + 0, + windowBounds.bottom / 2, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + SplitScreenMode.SPLIT_90_10 -> { + listOf( + DragZone.Split.Top( + bounds = + Rect( + 0, + 0, + windowBounds.right, + windowBounds.bottom - splitFromBubbleDragZoneHeight + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + 0, + windowBounds.bottom - splitFromBubbleDragZoneHeight, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + } + SplitScreenMode.SPLIT_10_90 -> { + listOf( + DragZone.Split.Top( + bounds = Rect(0, 0, windowBounds.right, splitFromBubbleDragZoneHeight), + ), + DragZone.Split.Bottom( + bounds = + Rect( + 0, + splitFromBubbleDragZoneHeight, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + } + } + } else { + when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.SPLIT_50_50, + SplitScreenMode.NONE -> + listOf( + DragZone.Split.Left( + bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom), + ), + DragZone.Split.Right( + bounds = + Rect( + windowBounds.right / 2, + 0, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + SplitScreenMode.SPLIT_90_10 -> + listOf( + DragZone.Split.Left( + bounds = + Rect( + 0, + 0, + windowBounds.right - splitFromBubbleDragZoneWidth, + windowBounds.bottom + ), + ), + DragZone.Split.Right( + bounds = + Rect( + windowBounds.right - splitFromBubbleDragZoneWidth, + 0, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + SplitScreenMode.SPLIT_10_90 -> + listOf( + DragZone.Split.Left( + bounds = Rect(0, 0, splitFromBubbleDragZoneWidth, windowBounds.bottom), + ), + DragZone.Split.Right( + bounds = + Rect( + splitFromBubbleDragZoneWidth, + 0, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + } + } + } + + private fun createSplitScreenDragZonesForExpandedViewOnTablet(): List<DragZone> { + return if (deviceConfig.isLandscape) { + createHorizontalSplitDragZonesForExpandedView() + } else { + // for tablets in portrait mode, split drag zones appear below the full screen drag zone + // for the top split zone, and above the dismiss zone. Both are horizontally centered. + val splitZoneLeft = windowBounds.right / 2 - vSplitFromExpandedViewDragZoneWidth / 2 + val splitZoneRight = splitZoneLeft + vSplitFromExpandedViewDragZoneWidth + val bottomSplitZoneBottom = windowBounds.bottom - dismissDragZoneSize + listOf( + DragZone.Split.Top( + bounds = + Rect( + splitZoneLeft, + fullScreenDragZoneHeight, + splitZoneRight, + fullScreenDragZoneHeight + vSplitFromExpandedViewDragZoneHeightTablet + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + splitZoneLeft, + bottomSplitZoneBottom - vSplitFromExpandedViewDragZoneHeightTablet, + splitZoneRight, + bottomSplitZoneBottom + ), + ) + ) + } + } + + private fun createSplitScreenDragZonesForExpandedViewOnFoldable(): List<DragZone> { + return if (deviceConfig.isLandscape) { + // vertical split drag zones are aligned with the full screen drag zone width + val splitZoneLeft = windowBounds.right / 2 - fullScreenDragZoneWidth / 2 + when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.SPLIT_50_50, + SplitScreenMode.NONE -> + listOf( + DragZone.Split.Top( + bounds = + Rect( + splitZoneLeft, + fullScreenDragZoneHeight, + splitZoneLeft + fullScreenDragZoneWidth, + fullScreenDragZoneHeight + + vSplitFromExpandedViewDragZoneHeightFold + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + splitZoneLeft, + windowBounds.bottom / 2, + splitZoneLeft + fullScreenDragZoneWidth, + windowBounds.bottom / 2 + + vSplitFromExpandedViewDragZoneHeightFold + ), + ) + ) + // TODO b/393172431: add this zone when it's defined + SplitScreenMode.SPLIT_10_90 -> listOf() + SplitScreenMode.SPLIT_90_10 -> + listOf( + DragZone.Split.Top( + bounds = + Rect( + splitZoneLeft, + fullScreenDragZoneHeight, + splitZoneLeft + fullScreenDragZoneWidth, + fullScreenDragZoneHeight + + vUnevenSplitFromExpandedViewDragZoneHeight + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + 0, + windowBounds.bottom - + vUnevenSplitFromExpandedViewDragZoneHeight, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + } + } else { + // horizontal split drag zones + createHorizontalSplitDragZonesForExpandedView() + } + } + + private fun createHorizontalSplitDragZonesForExpandedView(): List<DragZone> { + // horizontal split drag zones for expanded view appear on the edges of the screen from the + // top down until the dismiss drag zone height + return listOf( + DragZone.Split.Left( + bounds = + Rect( + 0, + 0, + hSplitFromExpandedViewDragZoneWidth, + windowBounds.bottom - dismissDragZoneSize + ), + ), + DragZone.Split.Right( + bounds = + Rect( + windowBounds.right - hSplitFromExpandedViewDragZoneWidth, + 0, + windowBounds.right, + windowBounds.bottom - dismissDragZoneSize + ), + ) + ) + } + + /** Checks the current split screen mode. */ + fun interface SplitScreenModeChecker { + enum class SplitScreenMode { + NONE, + SPLIT_50_50, + SPLIT_10_90, + SPLIT_90_10 + } + + fun getSplitScreenMode(): SplitScreenMode + } + + /** Checks if desktop window mode is supported. */ + fun interface DesktopWindowModeChecker { + fun isSupported(): Boolean + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt new file mode 100644 index 000000000000..028622798f34 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +/** A Bubble object being dragged. */ +sealed interface DraggedObject { + /** The initial location of the object at the start of the drag gesture. */ + val initialLocation: BubbleBarLocation + + data class Bubble(override val initialLocation: BubbleBarLocation) : DraggedObject + data class BubbleBar(override val initialLocation: BubbleBarLocation) : DraggedObject + data class ExpandedView(override val initialLocation: BubbleBarLocation) : DraggedObject +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java index 62ca5c687a2a..b1bc6e81e1bd 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java @@ -28,6 +28,7 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.hardware.HardwareBuffer; import android.util.TypedValue; import android.view.SurfaceControl; import android.window.TaskSnapshot; @@ -225,12 +226,17 @@ public abstract class PipContentOverlay { @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + final HardwareBuffer buffer = mBitmap.getHardwareBuffer(); tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); - tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); + tx.setBuffer(mLeash, buffer); tx.setAlpha(mLeash, 0f); tx.reparent(mLeash, parentLeash); tx.apply(); + // Cleanup the bitmap and buffer after setting up the leash + mBitmap.recycle(); + mBitmap = null; + buffer.close(); } @Override @@ -253,14 +259,6 @@ public abstract class PipContentOverlay { .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } - @Override - public void detach(SurfaceControl.Transaction tx) { - super.detach(tx); - if (mBitmap != null && !mBitmap.isRecycled()) { - mBitmap.recycle(); - } - } - private void prepareAppIconOverlay(Drawable appIcon) { final Canvas canvas = new Canvas(); canvas.setBitmap(mBitmap); @@ -282,7 +280,9 @@ public abstract class PipContentOverlay { mOverlayHalfSize + mAppIconSizePx / 2); appIcon.setBounds(appIconBounds); appIcon.draw(canvas); + Bitmap oldBitmap = mBitmap; mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); + oldBitmap.recycle(); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 947dbd276d3a..d77c177437b8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -192,10 +192,10 @@ public class Bubble implements BubbleViewProvider { * that bubble being added back to the stack anyways. */ @Nullable - private PendingIntent mIntent; - private boolean mIntentActive; + private PendingIntent mPendingIntent; + private boolean mPendingIntentActive; @Nullable - private PendingIntent.CancelListener mIntentCancelListener; + private PendingIntent.CancelListener mPendingIntentCancelListener; /** * Sent when the bubble & notification are no longer visible to the user (i.e. no @@ -205,12 +205,10 @@ public class Bubble implements BubbleViewProvider { private PendingIntent mDeleteIntent; /** - * Used only for a special bubble in the stack that has {@link #mIsAppBubble} set to true. - * There can only be one of these bubbles in the stack and this intent will be populated for - * that bubble. + * Used for app & note bubbles. */ @Nullable - private Intent mAppIntent; + private Intent mIntent; /** * Set while preparing a transition for animation. Several steps are needed before animation @@ -275,7 +273,7 @@ public class Bubble implements BubbleViewProvider { mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskId = INVALID_TASK_ID; - mAppIntent = intent; + mIntent = intent; mDesiredHeight = Integer.MAX_VALUE; mPackageName = intent.getPackage(); } @@ -294,7 +292,7 @@ public class Bubble implements BubbleViewProvider { mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskId = INVALID_TASK_ID; - mAppIntent = null; + mIntent = null; mDesiredHeight = Integer.MAX_VALUE; mPackageName = info.getPackage(); mShortcutInfo = info; @@ -319,7 +317,7 @@ public class Bubble implements BubbleViewProvider { mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskId = task.taskId; - mAppIntent = null; + mIntent = null; mDesiredHeight = Integer.MAX_VALUE; mPackageName = task.baseActivity.getPackageName(); } @@ -413,9 +411,9 @@ public class Bubble implements BubbleViewProvider { mGroupKey = entry.getGroupKey(); mLocusId = entry.getLocusId(); mBubbleMetadataFlagListener = listener; - mIntentCancelListener = intent -> { - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); + mPendingIntentCancelListener = intent -> { + if (mPendingIntent != null) { + mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); } mainExecutor.execute(() -> { intentCancelListener.onPendingIntentCanceled(this); @@ -601,10 +599,10 @@ public class Bubble implements BubbleViewProvider { if (cleanupTaskView) { cleanupTaskView(); } - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); + if (mPendingIntent != null) { + mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); } - mIntentActive = false; + mPendingIntentActive = false; } /** Cleans-up the taskview associated with this bubble (possibly removing the task from wm) */ @@ -874,19 +872,19 @@ public class Bubble implements BubbleViewProvider { mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); mIcon = entry.getBubbleMetadata().getIcon(); - if (!mIntentActive || mIntent == null) { - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); + if (!mPendingIntentActive || mPendingIntent == null) { + if (mPendingIntent != null) { + mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); } - mIntent = entry.getBubbleMetadata().getIntent(); - if (mIntent != null) { - mIntent.registerCancelListener(mIntentCancelListener); + mPendingIntent = entry.getBubbleMetadata().getIntent(); + if (mPendingIntent != null) { + mPendingIntent.registerCancelListener(mPendingIntentCancelListener); } - } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { + } else if (mPendingIntent != null && entry.getBubbleMetadata().getIntent() == null) { // Was an intent bubble now it's a shortcut bubble... still unregister the listener - mIntent.unregisterCancelListener(mIntentCancelListener); - mIntentActive = false; - mIntent = null; + mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); + mPendingIntentActive = false; + mPendingIntent = null; } mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); } @@ -926,12 +924,15 @@ public class Bubble implements BubbleViewProvider { * Sets if the intent used for this bubble is currently active (i.e. populating an * expanded view, expanded or not). */ - void setIntentActive() { - mIntentActive = true; + void setPendingIntentActive() { + mPendingIntentActive = true; } - boolean isIntentActive() { - return mIntentActive; + /** + * Whether the pending intent of this bubble is active (i.e. has been sent). + */ + boolean isPendingIntentActive() { + return mPendingIntentActive; } public InstanceId getInstanceId() { @@ -1118,9 +1119,12 @@ public class Bubble implements BubbleViewProvider { } } + /** + * Returns the pending intent used to populate the bubble. + */ @Nullable - PendingIntent getBubbleIntent() { - return mIntent; + PendingIntent getPendingIntent() { + return mPendingIntent; } /** @@ -1128,31 +1132,33 @@ public class Bubble implements BubbleViewProvider { * intent for an app. In this case we don't show a badge on the icon. */ public boolean isAppLaunchIntent() { - if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && mAppIntent != null) { - return mAppIntent.hasCategory("android.intent.category.LAUNCHER"); + if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && mIntent != null) { + return mIntent.hasCategory("android.intent.category.LAUNCHER"); } return false; } + /** + * Returns the pending intent to send when a bubble is dismissed (set via the notification API). + */ @Nullable PendingIntent getDeleteIntent() { return mDeleteIntent; } + /** + * Returns the intent used to populate the bubble. + */ @Nullable - @VisibleForTesting - public Intent getAppBubbleIntent() { - return mAppIntent; + public Intent getIntent() { + return mIntent; } /** - * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is - * true). - * - * @param appIntent The intent to set for the app bubble. + * Sets the intent used to populate the bubble. */ - void setAppBubbleIntent(Intent appIntent) { - mAppIntent = appIntent; + void setIntent(Intent intent) { + mIntent = intent; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 9120e0894ccf..2c81945ffdbe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -116,6 +116,7 @@ import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; import com.android.wm.shell.shared.bubbles.DeviceConfig; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -419,10 +420,11 @@ public class BubbleController implements ConfigurationChangeListener, mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mBubbleData.setPendingIntentCancelledListener(bubble -> { - if (bubble.getBubbleIntent() == null) { + if (bubble.getPendingIntent() == null) { return; } - if (bubble.isIntentActive() || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + if (bubble.isPendingIntentActive() + || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { bubble.setPendingIntentCanceled(); return; } @@ -924,6 +926,11 @@ public class BubbleController implements ConfigurationChangeListener, return mBubblePositioner; } + /** Provides bounds for drag zone drop targets */ + public BubbleDropTargetBoundsProvider getBubbleDropTargetBoundsProvider() { + return mBubblePositioner; + } + BubbleIconFactory getIconFactory() { return mBubbleIconFactory; } @@ -1663,7 +1670,7 @@ public class BubbleController implements ConfigurationChangeListener, // It's in the overflow, so remove it & reinflate mBubbleData.dismissBubbleWithKey(noteBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); // Update the bubble entry in the overflow with the latest intent. - b.setAppBubbleIntent(intent); + b.setIntent(intent); } else { // Notes bubble does not exist, lets add and expand it b = Bubble.createNotesBubble(intent, user, icon, mMainExecutor, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index ac74a42d1359..ad9ab7a722ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -237,8 +237,7 @@ public class BubbleExpandedView extends LinearLayout { PendingIntent pi = PendingIntent.getActivity( context, /* requestCode= */ 0, - mBubble.getAppBubbleIntent() - .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), + mBubble.getIntent().addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, @@ -252,7 +251,7 @@ public class BubbleExpandedView extends LinearLayout { } else { options.setLaunchedFromBubble(true); if (mBubble != null) { - mBubble.setIntentActive(); + mBubble.setPendingIntentActive(); } final Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. @@ -920,7 +919,7 @@ public class BubbleExpandedView extends LinearLayout { }); if (isNew) { - mPendingIntent = mBubble.getBubbleIntent(); + mPendingIntent = mBubble.getPendingIntent(); if ((mPendingIntent != null || mBubble.hasMetadataShortcutId()) && mTaskView != null) { setContentVisibility(false); @@ -947,7 +946,7 @@ public class BubbleExpandedView extends LinearLayout { */ private boolean didBackingContentChange(Bubble newBubble) { boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; - boolean newIsIntentBased = newBubble.getBubbleIntent() != null; + boolean newIsIntentBased = newBubble.getPendingIntent() != null; return prevWasIntentBased != newIsIntentBased; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 8cf3f7afd46a..5273a7cf2432 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -27,19 +27,21 @@ import android.graphics.RectF; import android.view.Surface; import android.view.WindowManager; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; import com.android.wm.shell.shared.bubbles.DeviceConfig; /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all * placement and positioning calculations to refer to. */ -public class BubblePositioner { +public class BubblePositioner implements BubbleDropTargetBoundsProvider { /** The screen edge the bubble stack is pinned to */ public enum StackPinnedEdge { @@ -100,6 +102,7 @@ public class BubblePositioner { private int mManageButtonHeight; private int mOverflowHeight; private int mMinimumFlyoutWidthLargeScreen; + private int mBubbleBarExpandedViewDropTargetPadding; private PointF mRestingStackPosition; @@ -164,6 +167,8 @@ public class BubblePositioner { res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width), mPositionRect.width() - 2 * mExpandedViewPadding ); + mBubbleBarExpandedViewDropTargetPadding = res.getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_drop_target_padding); if (mShowingInBubbleBar) { mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth; @@ -965,4 +970,14 @@ public class BubblePositioner { int top = getExpandedViewBottomForBubbleBar() - height; out.offsetTo(left, top); } + + @NonNull + @Override + public Rect getBubbleBarExpandedViewDropTargetBounds(boolean onLeft) { + Rect bounds = new Rect(); + getBubbleBarExpandedViewBounds(onLeft, false, bounds); + bounds.inset(mBubbleBarExpandedViewDropTargetPadding, + mBubbleBarExpandedViewDropTargetPadding); + return bounds; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 83d311ed6cd9..0d89bb260bf5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -119,7 +119,7 @@ public class BubbleTaskViewHelper { PendingIntent pi = PendingIntent.getActivity( context, /* requestCode= */ 0, - mBubble.getAppBubbleIntent() + mBubble.getIntent() .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, /* options= */ null); @@ -133,7 +133,7 @@ public class BubbleTaskViewHelper { } else { options.setLaunchedFromBubble(true); if (mBubble != null) { - mBubble.setIntentActive(); + mBubble.setPendingIntentActive(); } final Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. @@ -231,7 +231,7 @@ public class BubbleTaskViewHelper { boolean isNew = mBubble == null || didBackingContentChange(bubble); mBubble = bubble; if (isNew) { - mPendingIntent = mBubble.getBubbleIntent(); + mPendingIntent = mBubble.getPendingIntent(); return true; } return false; @@ -276,7 +276,7 @@ public class BubbleTaskViewHelper { */ private boolean didBackingContentChange(Bubble newBubble) { boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; - boolean newIsIntentBased = newBubble.getBubbleIntent() != null; + boolean newIsIntentBased = newBubble.getPendingIntent() != null; return prevWasIntentBased != newIsIntentBased; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt new file mode 100644 index 000000000000..41382047945b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.view.InputChannel +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<InputChannel>]. This can be used in place of kotlin default + * parameters values [builder = ::InputChannel] which requires the [@JvmOverloads] annotation to + * make this available in Java. + * This can be used every time a component needs the dependency to the default [Supplier] for + * [InputChannel]s. + */ +@WMSingleton +class InputChannelSupplier @Inject constructor() : Supplier<InputChannel> { + override fun get(): InputChannel = InputChannel() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt new file mode 100644 index 000000000000..2c66e97f03e1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.view.IWindowSession +import android.view.WindowManagerGlobal +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<IWindowSession>]. This can be used in place of kotlin default + * parameters values [builder = WindowManagerGlobal::getWindowSession] which requires the + * [@JvmOverloads] annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default [Supplier] for + * [IWindowSession]s. + */ +@WMSingleton +class WindowSessionSupplier @Inject constructor() : Supplier<IWindowSession> { + override fun get(): IWindowSession = WindowManagerGlobal.getWindowSession() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt new file mode 100644 index 000000000000..0b6c06ac5649 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.transition + +import android.view.SurfaceControl +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<SurfaceControl.Builder>]. This can be used in place of kotlin default + * parameters values [builder = ::SurfaceControl.Builder] which requires the [@JvmOverloads] + * annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default builder for + * [SurfaceControl]s. + */ +@WMSingleton +class SurfaceBuilderSupplier @Inject constructor() : Supplier<SurfaceControl.Builder> { + override fun get(): SurfaceControl.Builder = SurfaceControl.Builder() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt new file mode 100644 index 000000000000..2d9899b4fccf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.transition + +import android.view.SurfaceControl +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<SurfaceControl.Transaction>]. This can be used in place of kotlin default + * parameters values [builder = ::SurfaceControl.Transaction] which requires the [@JvmOverloads] + * annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default builder for + * [SurfaceControl.Transaction]s. + */ +@WMSingleton +class TransactionSupplier @Inject constructor() : Supplier<SurfaceControl.Transaction> { + override fun get(): SurfaceControl.Transaction = SurfaceControl.Transaction() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt new file mode 100644 index 000000000000..f7afbb5bdaef --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.view.GestureDetector.OnContextClickListener +import android.view.GestureDetector.OnDoubleTapListener +import android.view.GestureDetector.OnGestureListener +import android.view.MotionEvent + +/** + * Interface which unions all the interfaces related to gestures. + */ +interface LetterboxGestureListener : OnGestureListener, OnDoubleTapListener, OnContextClickListener + +/** + * Convenience class which provide an overrideable implementation of + * {@link LetterboxGestureListener}. + */ +object LetterboxGestureDelegate : LetterboxGestureListener { + override fun onDown(e: MotionEvent): Boolean = false + + override fun onShowPress(e: MotionEvent) { + } + + override fun onSingleTapUp(e: MotionEvent): Boolean = false + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean = false + + override fun onLongPress(e: MotionEvent) { + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean = false + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean = false + + override fun onDoubleTap(e: MotionEvent): Boolean = false + + override fun onDoubleTapEvent(e: MotionEvent): Boolean = false + + override fun onContextClick(e: MotionEvent): Boolean = false +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt new file mode 100644 index 000000000000..afd8e1519d24 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.content.Context +import android.graphics.Rect +import android.graphics.Region +import android.os.Handler +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.common.InputChannelSupplier +import com.android.wm.shell.common.WindowSessionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxUtils.Maps.runOnItem +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT +import java.util.function.Supplier +import javax.inject.Inject + +/** + * [LetterboxController] implementation responsible for handling the spy [SurfaceControl] we use + * to detect letterbox events. + */ +@WMSingleton +class LetterboxInputController @Inject constructor( + private val context: Context, + private val handler: Handler, + private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder, + private val listenerSupplier: Supplier<LetterboxGestureListener>, + private val windowSessionSupplier: WindowSessionSupplier, + private val inputChannelSupplier: InputChannelSupplier +) : LetterboxController { + + companion object { + @JvmStatic + private val TAG = "LetterboxInputController" + } + + private val inputDetectorMap = mutableMapOf<LetterboxKey, LetterboxInputDetector>() + + override fun createLetterboxSurface( + key: LetterboxKey, + transaction: Transaction, + parentLeash: SurfaceControl + ) { + inputDetectorMap.runOnItem(key, onMissed = { k, m -> + m[k] = + LetterboxInputDetector( + context, + handler, + listenerSupplier.get(), + inputSurfaceBuilder, + windowSessionSupplier, + inputChannelSupplier + ).apply { + start(transaction, parentLeash, key) + } + }) + } + + override fun destroyLetterboxSurface( + key: LetterboxKey, + transaction: Transaction + ) { + with(inputDetectorMap) { + runOnItem(key, onFound = { item -> + item.stop(transaction) + }) + remove(key) + } + } + + override fun updateLetterboxSurfaceVisibility( + key: LetterboxKey, + transaction: Transaction, + visible: Boolean + ) { + with(inputDetectorMap) { + runOnItem(key, onFound = { item -> + item.updateVisibility(transaction, visible) + }) + } + } + + override fun updateLetterboxSurfaceBounds( + key: LetterboxKey, + transaction: Transaction, + taskBounds: Rect, + activityBounds: Rect + ) { + inputDetectorMap.runOnItem(key, onFound = { item -> + item.updateTouchableRegion(transaction, Region(taskBounds)) + }) + } + + override fun dump() { + ProtoLog.v(WM_SHELL_APP_COMPAT, "%s: %s", TAG, "${inputDetectorMap.keys}") + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt new file mode 100644 index 000000000000..812cc0161aae --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt @@ -0,0 +1,230 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.content.Context +import android.graphics.Region +import android.os.Binder +import android.os.Handler +import android.os.IBinder +import android.os.RemoteException +import android.view.GestureDetector +import android.view.IWindowSession +import android.view.InputChannel +import android.view.InputEvent +import android.view.InputEventReceiver +import android.view.MotionEvent +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.WindowManager +import android.window.InputTransferToken +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.common.InputChannelSupplier +import com.android.wm.shell.common.WindowSessionSupplier +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT + +/** + * This is responsible for detecting events on a given [SurfaceControl]. + */ +class LetterboxInputDetector( + private val context: Context, + private val handler: Handler, + private val listener: LetterboxGestureListener, + private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder, + private val windowSessionSupplier: WindowSessionSupplier, + private val inputChannelSupplier: InputChannelSupplier +) { + + companion object { + @JvmStatic + private val TAG = "LetterboxInputDetector" + } + + private var state: InputDetectorState? = null + + fun start(tx: Transaction, source: SurfaceControl, key: LetterboxKey) { + if (!isRunning()) { + val tmpState = + InputDetectorState( + context, + handler, + source, + key.displayId, + listener, + inputSurfaceBuilder, + windowSessionSupplier.get(), + inputChannelSupplier + ) + if (tmpState.start(tx)) { + state = tmpState + } else { + ProtoLog.v( + WM_SHELL_APP_COMPAT, + "%s not started for %s on %s", + TAG, + "$source", + "$key" + ) + } + } + } + + fun updateTouchableRegion(tx: Transaction, region: Region) { + if (isRunning()) { + state?.setTouchableRegion(tx, region) + } + } + + fun isRunning() = state != null + + fun updateVisibility(tx: Transaction, visible: Boolean) { + if (isRunning()) { + state?.updateVisibility(tx, visible) + } + } + + fun stop(tx: Transaction) { + if (isRunning()) { + state!!.stop(tx) + state = null + } + } + + /** + * The state for a {@link SurfaceControl} for a given displayId. + */ + private class InputDetectorState( + val context: Context, + val handler: Handler, + val source: SurfaceControl, + val displayId: Int, + val listener: LetterboxGestureListener, + val inputSurfaceBuilder: LetterboxInputSurfaceBuilder, + val windowSession: IWindowSession, + inputChannelSupplier: InputChannelSupplier + ) { + + private val inputToken: IBinder + private val inputChannel: InputChannel + private var receiver: EventReceiver? = null + private var inputSurface: SurfaceControl? = null + + init { + inputToken = Binder() + inputChannel = inputChannelSupplier.get() + } + + fun start(tx: Transaction): Boolean { + val inputTransferToken = InputTransferToken() + try { + inputSurface = + inputSurfaceBuilder.createInputSurface( + tx, + source, + "Sink for $source", + "$TAG creation" + ) + windowSession.grantInputChannel( + displayId, + inputSurface, + inputToken, + null, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY, + WindowManager.LayoutParams.INPUT_FEATURE_SPY, + WindowManager.LayoutParams.TYPE_INPUT_CONSUMER, + null, + inputTransferToken, + "$TAG of $source", + inputChannel + ) + + receiver = EventReceiver(context, inputChannel, handler, listener) + return true + } catch (e: RemoteException) { + e.rethrowFromSystemServer() + } + return false + } + + fun setTouchableRegion(tx: Transaction, region: Region) { + try { + tx.setWindowCrop(inputSurface, region.bounds.width(), region.bounds.height()) + + windowSession.updateInputChannel( + inputChannel.token, + displayId, + inputSurface, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY, + WindowManager.LayoutParams.INPUT_FEATURE_SPY, + region + ) + } catch (e: RemoteException) { + e.rethrowFromSystemServer() + } + } + + fun updateVisibility(tx: Transaction, visible: Boolean) { + inputSurface?.let { + tx.setVisibility(it, visible) + } + } + + fun stop(tx: Transaction) { + receiver?.dispose() + receiver = null + inputChannel.dispose() + windowSession.removeToken(inputToken) + inputSurface?.let { s -> + tx.remove(s) + } + } + + // Removes the provided token + private fun IWindowSession.removeToken(token: IBinder) { + try { + remove(token) + } catch (e: RemoteException) { + e.rethrowFromSystemServer() + } + } + } + + /** + * Reads from the provided {@link InputChannel} and identifies a specific event. + */ + private class EventReceiver( + context: Context, + inputChannel: InputChannel, + uiHandler: Handler, + listener: LetterboxGestureListener + ) : InputEventReceiver(inputChannel, uiHandler.looper) { + private val eventDetector: GestureDetector + + init { + eventDetector = GestureDetector( + context, listener, + uiHandler + ) + } + + override fun onInputEvent(event: InputEvent) { + finishInputEvent(event, eventDetector.onTouchEvent(event as MotionEvent)) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt new file mode 100644 index 000000000000..fd8d86576115 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import com.android.wm.shell.common.transition.SurfaceBuilderSupplier +import com.android.wm.shell.dagger.WMSingleton +import javax.inject.Inject + +/** + * Component responsible for the actual creation of the Letterbox surfaces. + */ +@WMSingleton +class LetterboxInputSurfaceBuilder @Inject constructor( + private val surfaceBuilderSupplier: SurfaceBuilderSupplier +) { + + companion object { + /* + * Letterbox spy surfaces need to stay above the activity layer which is 0. + */ + // TODO(b/378673153): Consider adding this to [TaskConstants]. + @JvmStatic + private val TASK_CHILD_LAYER_LETTERBOX_SPY = 1000 + } + + fun createInputSurface( + tx: Transaction, + parentLeash: SurfaceControl, + surfaceName: String, + callSite: String + ) = surfaceBuilderSupplier.get() + .setName(surfaceName) + .setContainerLayer() + .setParent(parentLeash) + .setCallsite(callSite) + .build().apply { + tx.setLayer(this, TASK_CHILD_LAYER_LETTERBOX_SPY) + .setTrustedOverlay(this, true) + .show(this) + .apply() + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 621ccba40db2..27aed17762ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -60,6 +60,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; /** @@ -76,7 +77,11 @@ public class DesktopModeVisualIndicator { /** Indicates impending transition into split select on the left side */ TO_SPLIT_LEFT_INDICATOR, /** Indicates impending transition into split select on the right side */ - TO_SPLIT_RIGHT_INDICATOR + TO_SPLIT_RIGHT_INDICATOR, + /** Indicates impending transition into bubble on the left side */ + TO_BUBBLE_LEFT_INDICATOR, + /** Indicates impending transition into bubble on the right side */ + TO_BUBBLE_RIGHT_INDICATOR } /** @@ -115,6 +120,7 @@ public class DesktopModeVisualIndicator { private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer; private final ActivityManager.RunningTaskInfo mTaskInfo; private final SurfaceControl mTaskSurface; + private final @Nullable BubbleDropTargetBoundsProvider mBubbleBoundsProvider; private SurfaceControl mLeash; private final SyncTransactionQueue mSyncQueue; @@ -129,13 +135,15 @@ public class DesktopModeVisualIndicator { ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, - DragStartState dragStartState) { + DragStartState dragStartState, + @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { mSyncQueue = syncQueue; mTaskInfo = taskInfo; mDisplayController = displayController; mContext = context; mTaskSurface = taskSurface; mRootTdaOrganizer = taskDisplayAreaOrganizer; + mBubbleBoundsProvider = bubbleBoundsProvider; mCurrentType = NO_INDICATOR; mDragStartState = dragStartState; } @@ -175,15 +183,24 @@ public class DesktopModeVisualIndicator { captionHeight); final Region splitRightRegion = calculateSplitRightRegion(layout, transitionAreaWidth, captionHeight); - if (fullscreenRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { + final int x = (int) inputCoordinates.x; + final int y = (int) inputCoordinates.y; + if (fullscreenRegion.contains(x, y)) { result = TO_FULLSCREEN_INDICATOR; } - if (splitLeftRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { + if (splitLeftRegion.contains(x, y)) { result = IndicatorType.TO_SPLIT_LEFT_INDICATOR; } - if (splitRightRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { + if (splitRightRegion.contains(x, y)) { result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR; } + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + if (calculateBubbleLeftRegion(layout).contains(x, y)) { + result = IndicatorType.TO_BUBBLE_LEFT_INDICATOR; + } else if (calculateBubbleRightRegion(layout).contains(x, y)) { + result = IndicatorType.TO_BUBBLE_RIGHT_INDICATOR; + } + } if (mDragStartState != DragStartState.DRAGGED_INTENT) { transitionIndicator(result); } @@ -247,6 +264,25 @@ public class DesktopModeVisualIndicator { return region; } + @VisibleForTesting + Region calculateBubbleLeftRegion(DisplayLayout layout) { + int regionWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.bubble_transform_area_width); + int regionHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.bubble_transform_area_height); + return new Region(0, layout.height() - regionHeight, regionWidth, layout.height()); + } + + @VisibleForTesting + Region calculateBubbleRightRegion(DisplayLayout layout) { + int regionWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.bubble_transform_area_width); + int regionHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.bubble_transform_area_height); + return new Region(layout.width() - regionWidth, layout.height() - regionHeight, + layout.width(), layout.height()); + } + /** * Create a fullscreen indicator with no animation */ @@ -297,6 +333,11 @@ public class DesktopModeVisualIndicator { }); } + @VisibleForTesting + Rect getIndicatorBounds() { + return mView.getBackground().getBounds(); + } + /** * Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards. */ @@ -304,7 +345,8 @@ public class DesktopModeVisualIndicator { mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background); final VisualIndicatorAnimator animator = VisualIndicatorAnimator .fadeBoundsIn(mView, type, - mDisplayController.getDisplayLayout(mTaskInfo.displayId)); + mDisplayController.getDisplayLayout(mTaskInfo.displayId), + mBubbleBoundsProvider); animator.start(); mCurrentType = type; } @@ -323,7 +365,8 @@ public class DesktopModeVisualIndicator { } final VisualIndicatorAnimator animator = VisualIndicatorAnimator .fadeBoundsOut(mView, mCurrentType, - mDisplayController.getDisplayLayout(mTaskInfo.displayId)); + mDisplayController.getDisplayLayout(mTaskInfo.displayId), + mBubbleBoundsProvider); animator.start(); if (finishCallback != null) { animator.addListener(new AnimatorListenerAdapter() { @@ -351,7 +394,7 @@ public class DesktopModeVisualIndicator { } else { final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType( mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, - newType); + newType, mBubbleBoundsProvider); mCurrentType = newType; animator.start(); } @@ -406,8 +449,9 @@ public class DesktopModeVisualIndicator { } private static VisualIndicatorAnimator fadeBoundsIn( - @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout) { - final Rect endBounds = getIndicatorBounds(displayLayout, type); + @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout, + @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { + final Rect endBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider); final Rect startBounds = getMinBounds(endBounds); view.getBackground().setBounds(startBounds); @@ -419,8 +463,9 @@ public class DesktopModeVisualIndicator { } private static VisualIndicatorAnimator fadeBoundsOut( - @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout) { - final Rect startBounds = getIndicatorBounds(displayLayout, type); + @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout, + @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { + final Rect startBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider); final Rect endBounds = getMinBounds(startBounds); view.getBackground().setBounds(startBounds); @@ -435,16 +480,19 @@ public class DesktopModeVisualIndicator { * Create animator for visual indicator changing type (i.e., fullscreen to freeform, * freeform to split, etc.) * - * @param view the view for this indicator - * @param displayLayout information about the display the transitioning task is currently on - * @param origType the original indicator type - * @param newType the new indicator type + * @param view the view for this indicator + * @param displayLayout information about the display the transitioning task is + * currently on + * @param origType the original indicator type + * @param newType the new indicator type + * @param bubbleBoundsProvider provides bounds for bubbles indicators */ private static VisualIndicatorAnimator animateIndicatorType(@NonNull View view, - @NonNull DisplayLayout displayLayout, IndicatorType origType, - IndicatorType newType) { - final Rect startBounds = getIndicatorBounds(displayLayout, origType); - final Rect endBounds = getIndicatorBounds(displayLayout, newType); + @NonNull DisplayLayout displayLayout, IndicatorType origType, IndicatorType newType, + @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { + final Rect startBounds = getIndicatorBounds(displayLayout, origType, + bubbleBoundsProvider); + final Rect endBounds = getIndicatorBounds(displayLayout, newType, bubbleBoundsProvider); final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( view, startBounds, endBounds); animator.setInterpolator(new DecelerateInterpolator()); @@ -453,7 +501,8 @@ public class DesktopModeVisualIndicator { } /** Calculates the bounds the indicator should have when fully faded in. */ - private static Rect getIndicatorBounds(DisplayLayout layout, IndicatorType type) { + private static Rect getIndicatorBounds(DisplayLayout layout, IndicatorType type, + @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { final Rect desktopStableBounds = new Rect(); layout.getStableBounds(desktopStableBounds); final int padding = desktopStableBounds.top; @@ -481,6 +530,18 @@ public class DesktopModeVisualIndicator { return new Rect(desktopStableBounds.width() / 2 + padding, padding, desktopStableBounds.width() - padding, desktopStableBounds.height()); + case TO_BUBBLE_LEFT_INDICATOR: + if (bubbleBoundsProvider == null) { + return new Rect(); + } + return bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds( + /* onLeft= */ true); + case TO_BUBBLE_RIGHT_INDICATOR: + if (bubbleBoundsProvider == null) { + return new Rect(); + } + return bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds( + /* onLeft= */ false); default: throw new IllegalArgumentException("Invalid indicator type provided."); } 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 fb4016c4e7b6..0d32acd6b068 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 @@ -150,6 +150,7 @@ import java.util.Optional import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import java.util.function.Consumer +import kotlin.jvm.optionals.getOrNull /** Handles moving tasks in and out of desktop */ class DesktopTasksController( @@ -2706,6 +2707,7 @@ class DesktopTasksController( taskSurface, rootTaskDisplayAreaOrganizer, dragStartState, + bubbleController.getOrNull()?.bubbleDropTargetBoundsProvider, ) if (visualIndicator == null) visualIndicator = indicator return indicator.updateIndicatorType(PointF(inputX, taskTop)) @@ -2788,7 +2790,11 @@ class DesktopTasksController( desktopModeWindowDecoration, ) } - IndicatorType.NO_INDICATOR -> { + IndicatorType.NO_INDICATOR, + IndicatorType.TO_BUBBLE_LEFT_INDICATOR, + IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> { + // TODO(b/391928049): add support fof dragging desktop apps to a bubble + // Create a copy so that we can animate from the current bounds if we end up having // to snap the surface back without a WCT change. val destinationBounds = Rect(currentDragBounds) @@ -2915,6 +2921,11 @@ class DesktopTasksController( ) requestSplit(taskInfo, leftOrTop = false) } + IndicatorType.TO_BUBBLE_LEFT_INDICATOR, + IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> { + // TODO(b/388851898): move to bubble + cancelDragToDesktop(taskInfo) + } } return indicatorType } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index e74870d4d139..5894ea8d0b5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -32,6 +32,7 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.view.accessibility.AccessibilityManager; import android.window.SurfaceSyncGroup; import androidx.annotation.Nullable; @@ -63,6 +64,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private TvPipMenuView mPipMenuView; private TvPipBackgroundView mPipBackgroundView; + private final AccessibilityManager mA11yManager; + private boolean mIsReloading; private static final int PIP_MENU_FORCE_CLOSE_DELAY_MS = 10_000; private final Runnable mClosePipMenuRunnable = this::closeMenu; @@ -107,6 +110,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mSystemWindows = systemWindows; mMainHandler = mainHandler; + mA11yManager = context.getSystemService(AccessibilityManager.class); + // We need to "close" the menu the platform call for all the system dialogs to close (for // example, on the Home button press). final BroadcastReceiver closeSystemDialogsBroadcastReceiver = new BroadcastReceiver() { @@ -499,7 +504,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis switchToMenuMode(menuMode); } else { if (isMenuOpen(menuMode)) { - mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + if (!mA11yManager.isEnabled()) { + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + } mMenuModeOnFocus = menuMode; } // Send a request to gain window focus if the menu is open, or lose window focus @@ -594,8 +601,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis public void onUserInteracting() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onUserInteracting - mCurrentMenuMode=%s", TAG, getMenuModeString()); - mMainHandler.removeCallbacks(mClosePipMenuRunnable); - mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + if (mMainHandler.hasCallbacks(mClosePipMenuRunnable)) { + mMainHandler.removeCallbacks(mClosePipMenuRunnable); + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 9af23080351f..a6f872634ee9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -52,6 +52,7 @@ import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; @@ -571,7 +572,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - PendingTransition pending = findPending(transition); + final PendingTransition pending = findPending(transition); if (pending != null) { mPending.remove(pending); } @@ -586,10 +587,11 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV WindowContainerTransaction wct = null; for (int i = 0; i < info.getChanges().size(); ++i) { final TransitionInfo.Change chg = info.getChanges().get(i); - if (chg.getTaskInfo() == null) continue; + final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); + if (taskInfo == null) continue; if (TransitionUtil.isClosingType(chg.getMode())) { final boolean isHide = chg.getMode() == TRANSIT_TO_BACK; - TaskViewTaskController tv = findTaskView(chg.getTaskInfo()); + TaskViewTaskController tv = findTaskView(taskInfo); if (tv == null && !isHide) { // TaskView can be null when closing changesHandled++; @@ -599,7 +601,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV if (pending != null) { Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " + "shouldn't happen, so there may be a visual artifact: " - + chg.getTaskInfo().taskId); + + taskInfo.taskId); } continue; } @@ -615,40 +617,51 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV } changesHandled++; } else if (TransitionUtil.isOpeningType(chg.getMode())) { - final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN; - final TaskViewTaskController tv; - if (taskIsNew) { - if (pending == null - || !chg.getTaskInfo().containsLaunchCookie(pending.mLaunchCookie)) { + boolean isNewInTaskView = false; + TaskViewTaskController tv; + if (chg.getMode() == TRANSIT_OPEN) { + isNewInTaskView = true; + if (pending == null || !taskInfo.containsLaunchCookie(pending.mLaunchCookie)) { Slog.e(TAG, "Found a launching TaskView in the wrong transition. All " + "TaskView launches should be initiated by shell and in their " - + "own transition: " + chg.getTaskInfo().taskId); + + "own transition: " + taskInfo.taskId); continue; } stillNeedsMatchingLaunch = false; tv = pending.mTaskView; } else { - tv = findTaskView(chg.getTaskInfo()); - if (tv == null) { - if (pending != null) { - Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " - + "shouldn't happen, so there may be a visual artifact: " - + chg.getTaskInfo().taskId); + tv = findTaskView(taskInfo); + if (tv == null && pending != null) { + if (BubbleAnythingFlagHelper.enableCreateAnyBubble() + && chg.getMode() == TRANSIT_TO_FRONT + && pending.mTaskView.getPendingInfo() != null + && pending.mTaskView.getPendingInfo().taskId == taskInfo.taskId) { + // In this case an existing task, not currently in TaskView, is + // brought to the front to be moved into TaskView. This is still + // "new" from TaskView's perspective. (e.g. task being moved into a + // bubble) + isNewInTaskView = true; + stillNeedsMatchingLaunch = false; + tv = pending.mTaskView; + } else { + Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. " + + "This shouldn't happen, so there may be a visual " + + "artifact: " + taskInfo.taskId); } - continue; } + if (tv == null) continue; } if (wct == null) wct = new WindowContainerTransaction(); - prepareOpenAnimation(tv, taskIsNew, startTransaction, finishTransaction, - chg.getTaskInfo(), chg.getLeash(), wct); + prepareOpenAnimation(tv, isNewInTaskView, startTransaction, finishTransaction, + taskInfo, chg.getLeash(), wct); changesHandled++; } else if (chg.getMode() == TRANSIT_CHANGE) { - TaskViewTaskController tv = findTaskView(chg.getTaskInfo()); + TaskViewTaskController tv = findTaskView(taskInfo); if (tv == null) { if (pending != null) { Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " + "shouldn't happen, so there may be a visual artifact: " - + chg.getTaskInfo().taskId); + + taskInfo.taskId); } continue; } 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 72cbc4702ac8..c90f6cf62b7e 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 @@ -193,6 +193,9 @@ public class Transitions implements RemoteCallable<Transitions>, /** Transition to end the recents transition */ public static final int TRANSIT_END_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 22; + /** Transition type for app compat reachability. */ + public static final int TRANSIT_MOVE_LETTERBOX_REACHABILITY = TRANSIT_FIRST_CUSTOM + 23; + /** Transition type for desktop mode transitions. */ public static final int TRANSIT_DESKTOP_MODE_TYPES = WindowManager.TRANSIT_FIRST_CUSTOM + 100; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt new file mode 100644 index 000000000000..09c2faaa2670 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.testing.AndroidTestingRunner +import android.view.InputChannel +import androidx.test.filters.SmallTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [InputChannelSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:InputChannelSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class InputChannelSupplierTest { + + @Test + fun `InputChannelSupplier supplies an InputChannel`() { + val supplier = InputChannelSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is InputChannel + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt new file mode 100644 index 000000000000..8468c636542e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import java.util.function.Supplier + +/** + * Utility class we can use to test a []Supplier<T>] of any parameters type [T]. + */ +class SuppliersUtilsTest { + + companion object { + /** + * Allows to check that the object supplied is asserts what in [assertion]. + */ + fun <T> assertSupplierProvidesValue(supplier: Supplier<T>, assertion: (Any?) -> Boolean) { + assert(assertion(supplier.get())) { "Supplier didn't provided what is expected" } + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt new file mode 100644 index 000000000000..33e8d78d6a15 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.testing.AndroidTestingRunner +import android.view.IWindowSession +import androidx.test.filters.SmallTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [WindowSessionSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:WindowSessionSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class WindowSessionSupplierTest { + + @Test + fun `InputChannelSupplier supplies an InputChannel`() { + val supplier = WindowSessionSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is IWindowSession + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt new file mode 100644 index 000000000000..f88f72356759 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.transition + +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import androidx.test.filters.SmallTest +import com.android.wm.shell.common.SuppliersUtilsTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [SurfaceBuilderSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:SurfaceBuilderSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class SurfaceBuilderSupplierTest { + + @Test + fun `SurfaceBuilderSupplier supplies an SurfaceControl Builder`() { + val supplier = SurfaceBuilderSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is SurfaceControl.Builder + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt new file mode 100644 index 000000000000..12b4d8b5f96b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.transition + +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import androidx.test.filters.SmallTest +import com.android.wm.shell.common.SuppliersUtilsTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [TransactionSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:TransactionSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TransactionSupplierTest { + + @Test + fun `SurfaceBuilderSupplier supplies a Transaction`() { + val supplier = TransactionSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is SurfaceControl.Transaction + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt index 88cc981dd30c..e34884b103f6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt @@ -33,10 +33,10 @@ abstract class LetterboxControllerRobotTest { companion object { @JvmStatic - private val DISPLAY_ID = 1 + val DISPLAY_ID = 1 @JvmStatic - private val TASK_ID = 20 + val TASK_ID = 20 } lateinit var letterboxController: LetterboxController diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt new file mode 100644 index 000000000000..bc3416a88918 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.verify + +/** + * Tests for [LetterboxGestureDelegate]. + * + * Build/Install/Run: + * atest WMShellUnitTests:LetterboxGestureDelegateTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LetterboxGestureDelegateTest { + + class DelegateTest : LetterboxGestureListener by LetterboxGestureDelegate + + val delegate = DelegateTest() + + @Before + fun setUp() { + spyOn(LetterboxGestureDelegate) + } + + @Test + fun `When delegating all methods are invoked`() { + val event = motionEventAt(0f, 0f) + with(delegate) { + onDown(event) + onShowPress(event) + onSingleTapUp(event) + onScroll(event, event, 0f, 0f) + onFling(event, event, 0f, 0f) + onLongPress(event) + onSingleTapConfirmed(event) + onDoubleTap(event) + onDoubleTapEvent(event) + onContextClick(event) + } + with(LetterboxGestureDelegate) { + verify(this).onDown(event) + verify(this).onShowPress(event) + verify(this).onSingleTapUp(event) + verify(this).onScroll(event, event, 0f, 0f) + verify(this).onFling(event, event, 0f, 0f) + verify(this).onLongPress(event) + verify(this).onSingleTapConfirmed(event) + verify(this).onDoubleTap(event) + verify(this).onDoubleTapEvent(event) + verify(this).onContextClick(event) + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt new file mode 100644 index 000000000000..fa95faee4b6e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.content.Context +import android.graphics.Rect +import android.graphics.Region +import android.os.Handler +import android.os.Looper +import android.testing.AndroidTestingRunner +import android.view.IWindowSession +import android.view.InputChannel +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.InputChannelSupplier +import com.android.wm.shell.common.WindowSessionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxMatchers.asAnyMode +import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.TAG +import java.util.function.Consumer +import java.util.function.Supplier +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +/** + * Tests for [LetterboxInputController]. + * + * Build/Install/Run: + * atest WMShellUnitTests:LetterboxInputControllerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LetterboxInputControllerTest : ShellTestCase() { + + @Test + fun `When creation is requested the surface is created if not present`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + + r.checkInputSurfaceBuilderInvoked() + } + } + + @Test + fun `When creation is requested multiple times the input surface is created once`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + + r.checkInputSurfaceBuilderInvoked(times = 1) + } + } + + @Test + fun `A different input surface is created for every key`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest(displayId = 2) + r.sendCreateSurfaceRequest(displayId = 2, taskId = 2) + r.sendCreateSurfaceRequest(displayId = 2) + r.sendCreateSurfaceRequest(displayId = 2, taskId = 2) + + r.checkInputSurfaceBuilderInvoked(times = 3) + } + } + + @Test + fun `Created spy surface is removed once`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.checkInputSurfaceBuilderInvoked() + + r.sendDestroySurfaceRequest() + r.sendDestroySurfaceRequest() + r.sendDestroySurfaceRequest() + + r.checkTransactionRemovedInvoked() + } + } + @Test + fun `Only existing surfaces receive visibility update`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendUpdateSurfaceVisibilityRequest(visible = true) + r.sendUpdateSurfaceVisibilityRequest(visible = true, displayId = 20) + + r.checkVisibilityUpdated(expectedVisibility = true) + } + } + + @Test + fun `Only existing surfaces receive taskBounds update`() { + runTestScenario { r -> + r.sendUpdateSurfaceBoundsRequest( + taskBounds = Rect(0, 0, 2000, 1000), + activityBounds = Rect(500, 0, 1500, 1000) + ) + + r.checkUpdateSessionRegion(times = 0, region = Region(0, 0, 2000, 1000)) + r.checkSurfaceSizeUpdated(times = 0, expectedWidth = 2000, expectedHeight = 1000) + + r.resetTransitionTest() + + r.sendCreateSurfaceRequest() + r.sendUpdateSurfaceBoundsRequest( + taskBounds = Rect(0, 0, 2000, 1000), + activityBounds = Rect(500, 0, 1500, 1000) + ) + r.checkUpdateSessionRegion(region = Region(0, 0, 2000, 1000)) + r.checkSurfaceSizeUpdated(expectedWidth = 2000, expectedHeight = 1000) + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<InputLetterboxControllerRobotTest>) { + consumer.accept(InputLetterboxControllerRobotTest(mContext).apply { initController() }) + } + + class InputLetterboxControllerRobotTest(private val context: Context) : + LetterboxControllerRobotTest() { + + private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder + private val handler = Handler(Looper.getMainLooper()) + private val listener: LetterboxGestureListener + private val listenerSupplier: Supplier<LetterboxGestureListener> + private val windowSessionSupplier: WindowSessionSupplier + private val windowSession: IWindowSession + private val inputChannelSupplier: InputChannelSupplier + + init { + inputSurfaceBuilder = getLetterboxInputSurfaceBuilderMock() + listener = mock<LetterboxGestureListener>() + listenerSupplier = mock<Supplier<LetterboxGestureListener>>() + doReturn(LetterboxGestureDelegate).`when`(listenerSupplier).get() + windowSessionSupplier = mock<WindowSessionSupplier>() + windowSession = mock<IWindowSession>() + doReturn(windowSession).`when`(windowSessionSupplier).get() + inputChannelSupplier = mock<InputChannelSupplier>() + val inputChannels = InputChannel.openInputChannelPair(TAG) + inputChannels.first().dispose() + doReturn(inputChannels[1]).`when`(inputChannelSupplier).get() + } + + override fun buildController(): LetterboxController = + LetterboxInputController( + context, + handler, + inputSurfaceBuilder, + listenerSupplier, + windowSessionSupplier, + inputChannelSupplier + ) + + fun checkInputSurfaceBuilderInvoked( + times: Int = 1, + name: String = "", + callSite: String = "" + ) { + verify(inputSurfaceBuilder, times(times)).createInputSurface( + eq(transaction), + eq(parentLeash), + name.asAnyMode(), + callSite.asAnyMode() + ) + } + + fun checkUpdateSessionRegion(times: Int = 1, displayId: Int = DISPLAY_ID, region: Region) { + verify(windowSession, times(times)).updateInputChannel( + any(), + eq(displayId), + any(), + any(), + any(), + any(), + eq(region) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt index 2c06dfda7917..3ce1fec32a16 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt @@ -16,6 +16,8 @@ package com.android.wm.shell.compatui.letterbox +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.obtain import android.view.SurfaceControl import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -37,6 +39,18 @@ fun getTransactionMock(): SurfaceControl.Transaction = mock<SurfaceControl.Trans doReturn(this).`when`(this).setWindowCrop(anyOrNull(), any(), any()) } +/** + * @return A [LetterboxInputSurfaceBuilder] mock to use in tests. + */ +fun getLetterboxInputSurfaceBuilderMock() = mock<LetterboxInputSurfaceBuilder>().apply { + doReturn(SurfaceControl()).`when`(this).createInputSurface( + any(), + any(), + any(), + any() + ) +} + // Utility to make verification mode depending on a [Boolean]. fun Boolean.asMode(): VerificationMode = if (this) times(1) else never() @@ -47,5 +61,10 @@ object LetterboxMatchers { fun String.asAnyMode() = asAnyMode { this.isEmpty() } } +object LetterboxEvents { + fun motionEventAt(x: Float, y: Float) = + obtain(0, 10, ACTION_DOWN, x, y, 0) +} + private inline fun <reified T : Any> T.asAnyMode(condition: () -> Boolean) = (if (condition()) any() else eq(this)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index 13b44977e9c7..a6575535faee 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -16,11 +16,11 @@ package com.android.wm.shell.desktopmode +import android.animation.AnimatorTestRule import android.app.ActivityManager.RunningTaskInfo import android.graphics.PointF import android.graphics.Rect import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl @@ -34,6 +34,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -56,7 +57,7 @@ import org.mockito.kotlin.whenever @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopModeVisualIndicatorTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() + @JvmField @Rule val animatorTestRule = AnimatorTestRule(this) @JvmField @Rule @@ -69,6 +70,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { @Mock private lateinit var taskSurface: SurfaceControl @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var displayLayout: DisplayLayout + @Mock private lateinit var bubbleBoundsProvider: BubbleDropTargetBoundsProvider private lateinit var visualIndicator: DesktopModeVisualIndicator @@ -80,6 +82,8 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display) + whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(any())) + .thenReturn(Rect()) taskInfo = DesktopTestHelpers.createFullscreenTask() } @@ -194,6 +198,40 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { } @Test + fun testBubbleLeftRegionCalculation() { + val bubbleRegionWidth = + context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_width) + val bubbleRegionHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_height) + val expectedRect = Rect(0, 1600 - bubbleRegionHeight, bubbleRegionWidth, 1600) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + var testRegion = visualIndicator.calculateBubbleLeftRegion(displayLayout) + assertThat(testRegion.bounds).isEqualTo(expectedRect) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + testRegion = visualIndicator.calculateBubbleLeftRegion(displayLayout) + assertThat(testRegion.bounds).isEqualTo(expectedRect) + } + + @Test + fun testBubbleRightRegionCalculation() { + val bubbleRegionWidth = + context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_width) + val bubbleRegionHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_height) + val expectedRect = Rect(2400 - bubbleRegionWidth, 1600 - bubbleRegionHeight, 2400, 1600) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + var testRegion = visualIndicator.calculateBubbleRightRegion(displayLayout) + assertThat(testRegion.bounds).isEqualTo(expectedRect) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + testRegion = visualIndicator.calculateBubbleRightRegion(displayLayout) + assertThat(testRegion.bounds).isEqualTo(expectedRect) + } + + @Test fun testDefaultIndicators() { createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f)) @@ -219,31 +257,79 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { fun testDefaultIndicatorWithNoDesktop() { whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) + // Fullscreen to center, no desktop indicator createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) - + // Fullscreen to split result = visualIndicator.updateIndicatorType(PointF(10000f, 500f)) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) - result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f)) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) - + // Fullscreen to bubble + result = visualIndicator.updateIndicatorType(PointF(100f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR) + // Split to center, no desktop indicator createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) - + // Split to fullscreen result = visualIndicator.updateIndicatorType(PointF(500f, 0f)) assertThat(result) .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) - + // Split to bubble + result = visualIndicator.updateIndicatorType(PointF(100f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR) + // Drag app to center, no desktop indicator createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) } + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testBubbleLeftVisualIndicatorSize() { + val dropTargetBounds = Rect(100, 100, 500, 1500) + whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(/* onLeft= */ true)) + .thenReturn(dropTargetBounds) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + visualIndicator.updateIndicatorType(PointF(100f, 1500f)) + + animatorTestRule.advanceTimeBy(200) + + assertThat(visualIndicator.indicatorBounds).isEqualTo(dropTargetBounds) + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testBubbleRightVisualIndicatorSize() { + val dropTargetBounds = Rect(1900, 100, 2300, 1500) + whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(/* onLeft= */ false)) + .thenReturn(dropTargetBounds) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + + animatorTestRule.advanceTimeBy(200) + + assertThat(visualIndicator.indicatorBounds).isEqualTo(dropTargetBounds) + } + private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) { visualIndicator = DesktopModeVisualIndicator( @@ -254,6 +340,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { taskSurface, taskDisplayAreaOrganizer, dragStartState, + bubbleBoundsProvider, ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt new file mode 100644 index 000000000000..e28d6ff8bf7f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.graphics.Insets +import android.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +/** Unit tests for [DragZoneFactory]. */ +class DragZoneFactoryTest { + + private lateinit var dragZoneFactory: DragZoneFactory + private val tabletPortrait = + DeviceConfig( + windowBounds = Rect(0, 0, 1000, 2000), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = false, + isRtl = false, + insets = Insets.of(0, 0, 0, 0) + ) + private val tabletLandscape = + tabletPortrait.copy(windowBounds = Rect(0, 0, 2000, 1000), isLandscape = true) + private val foldablePortrait = + tabletPortrait.copy(windowBounds = Rect(0, 0, 800, 900), isSmallTablet = true) + private val foldableLandscape = + foldablePortrait.copy(windowBounds = Rect(0, 0, 900, 800), isLandscape = true) + private val splitScreenModeChecker = SplitScreenModeChecker { SplitScreenMode.NONE } + private var isDesktopWindowModeSupported = true + private val desktopWindowModeChecker = DesktopWindowModeChecker { isDesktopWindowModeSupported } + + @Test + fun dragZonesForBubbleBar_tablet() { + dragZoneFactory = + DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT)) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.Bubble::class.java, + DragZone.Bubble::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForBubble_tablet_portrait() { + dragZoneFactory = + DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.Bubble.Left::class.java, + DragZone.Bubble.Right::class.java, + DragZone.FullScreen::class.java, + DragZone.DesktopWindow::class.java, + DragZone.Split.Top::class.java, + DragZone.Split.Bottom::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForBubble_tablet_landscape() { + dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.Bubble.Left::class.java, + DragZone.Bubble.Right::class.java, + DragZone.FullScreen::class.java, + DragZone.DesktopWindow::class.java, + DragZone.Split.Left::class.java, + DragZone.Split.Right::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForBubble_foldable_portrait() { + dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.Bubble.Left::class.java, + DragZone.Bubble.Right::class.java, + DragZone.FullScreen::class.java, + DragZone.Split.Left::class.java, + DragZone.Split.Right::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForBubble_foldable_landscape() { + dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.Bubble.Left::class.java, + DragZone.Bubble.Right::class.java, + DragZone.FullScreen::class.java, + DragZone.Split.Top::class.java, + DragZone.Split.Bottom::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForExpandedView_tablet_portrait() { + dragZoneFactory = + DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.FullScreen::class.java, + DragZone.DesktopWindow::class.java, + DragZone.Split.Top::class.java, + DragZone.Split.Bottom::class.java, + DragZone.Bubble.Left::class.java, + DragZone.Bubble.Right::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForExpandedView_tablet_landscape() { + dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.FullScreen::class.java, + DragZone.DesktopWindow::class.java, + DragZone.Split.Left::class.java, + DragZone.Split.Right::class.java, + DragZone.Bubble.Left::class.java, + DragZone.Bubble.Right::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForExpandedView_foldable_portrait() { + dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.FullScreen::class.java, + DragZone.Split.Left::class.java, + DragZone.Split.Right::class.java, + DragZone.Bubble.Left::class.java, + DragZone.Bubble.Right::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForExpandedView_foldable_landscape() { + dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + val expectedZones: List<Class<out DragZone>> = + listOf( + DragZone.Dismiss::class.java, + DragZone.FullScreen::class.java, + DragZone.Split.Top::class.java, + DragZone.Split.Bottom::class.java, + DragZone.Bubble.Left::class.java, + DragZone.Bubble.Right::class.java, + ) + dragZones.zip(expectedZones).forEach { (zone, expectedType) -> + assertThat(zone).isInstanceOf(expectedType) + } + } + + @Test + fun dragZonesForBubble_tablet_desktopModeDisabled() { + isDesktopWindowModeSupported = false + dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() + } + + @Test + fun dragZonesForExpandedView_tablet_desktopModeDisabled() { + isDesktopWindowModeSupported = false + dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT)) + assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() + } +} diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 3bc238a812d9..61c287b9633c 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -1232,63 +1232,73 @@ static void AMessageToCryptoInfo(JNIEnv * env, const jobject & obj, sp<ABuffer> ivBuffer; CryptoPlugin::Mode mode; CryptoPlugin::Pattern pattern; - CHECK(msg->findInt32("mode", (int*)&mode)); - CHECK(msg->findSize("numSubSamples", &numSubSamples)); - CHECK(msg->findBuffer("subSamples", &subSamplesBuffer)); - CHECK(msg->findInt32("encryptBlocks", (int32_t *)&pattern.mEncryptBlocks)); - CHECK(msg->findInt32("skipBlocks", (int32_t *)&pattern.mSkipBlocks)); - CHECK(msg->findBuffer("iv", &ivBuffer)); - CHECK(msg->findBuffer("key", &keyBuffer)); - - // subsamples + CryptoPlugin::SubSample *samplesArray = nullptr; + ScopedLocalRef<jbyteArray> keyArray(env, env->NewByteArray(16)); + ScopedLocalRef<jbyteArray> ivArray(env, env->NewByteArray(16)); + jboolean isCopy; + sp<RefBase> cryptoInfosObj; + if (msg->findObject("cryptoInfos", &cryptoInfosObj)) { + sp<CryptoInfosWrapper> cryptoInfos((CryptoInfosWrapper*)cryptoInfosObj.get()); + CHECK(!cryptoInfos->value.empty() && (cryptoInfos->value[0] != nullptr)); + std::unique_ptr<CodecCryptoInfo> &info = cryptoInfos->value[0]; + mode = info->mMode; + numSubSamples = info->mNumSubSamples; + samplesArray = info->mSubSamples; + pattern = info->mPattern; + if (info->mKey != nullptr) { + jbyte * dstKey = env->GetByteArrayElements(keyArray.get(), &isCopy); + memcpy(dstKey, info->mKey, 16); + env->ReleaseByteArrayElements(keyArray.get(), dstKey, 0); + } + if (info->mIv != nullptr) { + jbyte * dstIv = env->GetByteArrayElements(ivArray.get(), &isCopy); + memcpy(dstIv, info->mIv, 16); + env->ReleaseByteArrayElements(ivArray.get(), dstIv, 0); + } + } else { + CHECK(msg->findInt32("mode", (int*)&mode)); + CHECK(msg->findSize("numSubSamples", &numSubSamples)); + CHECK(msg->findBuffer("subSamples", &subSamplesBuffer)); + CHECK(msg->findInt32("encryptBlocks", (int32_t *)&pattern.mEncryptBlocks)); + CHECK(msg->findInt32("skipBlocks", (int32_t *)&pattern.mSkipBlocks)); + CHECK(msg->findBuffer("iv", &ivBuffer)); + CHECK(msg->findBuffer("key", &keyBuffer)); + samplesArray = + (CryptoPlugin::SubSample*)(subSamplesBuffer.get()->data()); + if (keyBuffer.get() != nullptr && keyBuffer->size() > 0) { + jbyte * dstKey = env->GetByteArrayElements(keyArray.get(), &isCopy); + memcpy(dstKey, keyBuffer->data(), keyBuffer->size()); + env->ReleaseByteArrayElements(keyArray.get(), dstKey, 0); + } + if (ivBuffer.get() != nullptr && ivBuffer->size() > 0) { + jbyte * dstIv = env->GetByteArrayElements(ivArray.get(), &isCopy); + memcpy(dstIv, ivBuffer->data(), ivBuffer->size()); + env->ReleaseByteArrayElements(ivArray.get(), dstIv, 0); + } + } ScopedLocalRef<jintArray> samplesOfEncryptedDataArr(env, env->NewIntArray(numSubSamples)); ScopedLocalRef<jintArray> samplesOfClearDataArr(env, env->NewIntArray(numSubSamples)); - jboolean isCopy; - jint *dstEncryptedSamples = - env->GetIntArrayElements(samplesOfEncryptedDataArr.get(), &isCopy); - jint * dstClearSamples = - env->GetIntArrayElements(samplesOfClearDataArr.get(), &isCopy); - - CryptoPlugin::SubSample * samplesArray = - (CryptoPlugin::SubSample*)(subSamplesBuffer.get()->data()); - - for(int i = 0 ; i < numSubSamples ; i++) { - dstEncryptedSamples[i] = samplesArray[i].mNumBytesOfEncryptedData; - dstClearSamples[i] = samplesArray[i].mNumBytesOfClearData; - } - env->ReleaseIntArrayElements(samplesOfEncryptedDataArr.get(), dstEncryptedSamples, 0); - env->ReleaseIntArrayElements(samplesOfClearDataArr.get(), dstClearSamples, 0); - // key and iv - jbyteArray keyArray = NULL; - jbyteArray ivArray = NULL; - if (keyBuffer.get() != nullptr && keyBuffer->size() > 0) { - keyArray = env->NewByteArray(keyBuffer->size()); - jbyte * dstKey = env->GetByteArrayElements(keyArray, &isCopy); - memcpy(dstKey, keyBuffer->data(), keyBuffer->size()); - env->ReleaseByteArrayElements(keyArray,dstKey,0); - } - if (ivBuffer.get() != nullptr && ivBuffer->size() > 0) { - ivArray = env->NewByteArray(ivBuffer->size()); - jbyte *dstIv = env->GetByteArrayElements(ivArray, &isCopy); - memcpy(dstIv, ivBuffer->data(), ivBuffer->size()); - env->ReleaseByteArrayElements(ivArray, dstIv,0); - } - // set samples, key and iv + if (numSubSamples > 0) { + jint *dstEncryptedSamples = + env->GetIntArrayElements(samplesOfEncryptedDataArr.get(), &isCopy); + jint * dstClearSamples = + env->GetIntArrayElements(samplesOfClearDataArr.get(), &isCopy); + for(int i = 0 ; i < numSubSamples ; i++) { + dstEncryptedSamples[i] = samplesArray[i].mNumBytesOfEncryptedData; + dstClearSamples[i] = samplesArray[i].mNumBytesOfClearData; + } + env->ReleaseIntArrayElements(samplesOfEncryptedDataArr.get(), dstEncryptedSamples, 0); + env->ReleaseIntArrayElements(samplesOfClearDataArr.get(), dstClearSamples, 0); + } env->CallVoidMethod( obj, gFields.cryptoInfoSetID, (jint)numSubSamples, samplesOfClearDataArr.get(), samplesOfEncryptedDataArr.get(), - keyArray, - ivArray, + keyArray.get(), + ivArray.get(), mode); - if (keyArray != NULL) { - env->DeleteLocalRef(keyArray); - } - if (ivArray != NULL) { - env->DeleteLocalRef(ivArray); - } // set pattern env->CallVoidMethod( obj, diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index 4b407c50bbd5..af40c647e805 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -73,6 +73,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi private boolean mLottieDynamicColor; private CharSequence mContentDescription; private boolean mIsTablet; + private boolean mIsAnimationPaused; /** * Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs. @@ -143,6 +144,16 @@ public class IllustrationPreference extends Preference implements GroupSectionDi (FrameLayout) holder.findViewById(R.id.middleground_layout); final LottieAnimationView illustrationView = (LottieAnimationView) holder.findViewById(R.id.lottie_view); + // Pause and resume animation + illustrationFrame.setOnClickListener(v -> { + mIsAnimationPaused = !mIsAnimationPaused; + if (mIsAnimationPaused) { + illustrationView.pauseAnimation(); + } else { + illustrationView.resumeAnimation(); + } + }); + if (illustrationView != null && !TextUtils.isEmpty(mContentDescription)) { illustrationView.setContentDescription(mContentDescription); illustrationView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt index 13d551aef4c2..a840a6f0476f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp @@ -71,6 +72,8 @@ constructor( return } + val buttonSize = dimensionResource(R.dimen.communal_to_dream_button_size) + if (viewModel.shouldShowTooltip) { Column( modifier = @@ -96,7 +99,6 @@ constructor( } companion object { - private val buttonSize = 64.dp private val tooltipMaxWidth = 350.dp } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt index 34c0bcaca997..ae541dda6eeb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt @@ -26,8 +26,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.contains @@ -46,6 +46,28 @@ import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import javax.inject.Inject +@Composable +fun ClockView(view: View?, modifier: Modifier = Modifier) { + AndroidView( + factory = { + FrameLayout(it).apply { + // Clip nothing. The clock views at times render outside their bounds. Compose does + // not clip by default, so only this layer needs clipping to be explicitly disabled. + clipChildren = false + clipToPadding = false + } + }, + update = { parent -> + view?.let { + parent.removeAllViews() + (view.parent as? ViewGroup)?.removeView(view) + parent.addView(view) + } ?: run { parent.removeAllViews() } + }, + modifier = modifier, + ) +} + /** Provides small clock and large clock composables for the default clock face. */ class DefaultClockSection @Inject @@ -67,14 +89,9 @@ constructor( if (currentClock?.smallClock?.view == null) { return } - val context = LocalContext.current - AndroidView( - factory = { context -> - FrameLayout(context).apply { - ensureClockViewExists(checkNotNull(currentClock).smallClock.view) - } - }, - update = { it.ensureClockViewExists(checkNotNull(currentClock).smallClock.view) }, + + ClockView( + checkNotNull(currentClock).smallClock.view, modifier = modifier .height(dimensionResource(R.dimen.small_clock_height)) @@ -116,25 +133,8 @@ constructor( Element(key = largeClockElementKey, modifier = modifier) { content { - AndroidView( - factory = { context -> - FrameLayout(context).apply { - // By default, ViewGroups like FrameLayout clip their children. Turning - // off the clipping allows the child view to render outside of its - // bounds - letting the step animation of the clock push the digits out - // when needed. - // - // Note that, in Compose, clipping is actually disabled by default so - // there's no need to propagate this up the composable hierarchy. - clipChildren = false - clipToPadding = false - - ensureClockViewExists(checkNotNull(currentClock).largeClock.view) - } - }, - update = { - it.ensureClockViewExists(checkNotNull(currentClock).largeClock.view) - }, + ClockView( + checkNotNull(currentClock).largeClock.view, modifier = Modifier.fillMaxSize() .burnInAware( @@ -147,15 +147,6 @@ constructor( } } - private fun FrameLayout.ensureClockViewExists(clockView: View) { - if (contains(clockView)) { - return - } - removeAllViews() - (clockView.parent as? ViewGroup)?.removeView(clockView) - addView(clockView) - } - fun getClockCenteringDistance(): Float { return Resources.getSystem().displayMetrics.widthPixels / 4f } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt index 6250da379402..4fcb5ca42df2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt @@ -16,8 +16,6 @@ package com.android.systemui.keyguard.ui.composable.section -import android.view.View -import android.view.ViewGroup import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row @@ -26,10 +24,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.systemui.customization.R as customR @@ -112,21 +110,11 @@ constructor( ) { Element(key = elementKey, modifier) { content { - AndroidView( - factory = { - try { - val view = - clock.largeClock.layout.views.first { - it.id == weatherClockElementViewId - } - (view.parent as? ViewGroup)?.removeView(view) - view - } catch (e: NoSuchElementException) { - View(it) - } + ClockView( + clock.largeClock.layout.views.firstOrNull { + it.id == weatherClockElementViewId }, - update = {}, - modifier = modifier, + modifier, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt index c73656eb1ec5..f1cc71bc59af 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt @@ -16,9 +16,9 @@ package com.android.systemui.volume.panel.component.mediaoutput -import com.android.systemui.volume.panel.component.mediaoutput.domain.MediaOutputAvailabilityCriteria import com.android.systemui.volume.panel.component.mediaoutput.ui.composable.MediaOutputComponent import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents +import com.android.systemui.volume.panel.domain.AlwaysAvailableCriteria import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent import dagger.Binds @@ -39,6 +39,6 @@ interface MediaOutputModule { @IntoMap @StringKey(VolumePanelComponents.MEDIA_OUTPUT) fun bindComponentAvailabilityCriteria( - criteria: MediaOutputAvailabilityCriteria + criteria: AlwaysAvailableCriteria ): ComponentAvailabilityCriteria } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index ab31286b78b4..b59b4ab34c80 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -145,7 +145,6 @@ open class ClockRegistry( var isCurrentClock = false var isClockListChanged = false for (metadata in knownClocks) { - isCurrentClock = isCurrentClock || currentClockId == metadata.clockId val id = metadata.clockId val info = availableClocks.concurrentGetOrPut(id, ClockInfo(metadata, null, manager)) { @@ -156,15 +155,17 @@ open class ClockRegistry( if (manager != info.manager) { logger.e({ "Clock Id conflict on attach: " + - "$str1 is double registered by $str2 and $str3" + "$str1 is double registered by $str2 and $str3. " + + "Using $str2 since it was attached first." }) { str1 = id - str2 = info.manager.toString() + str2 = info.manager?.toString() ?: info.provider?.toString() str3 = manager.toString() } continue } + isCurrentClock = isCurrentClock || currentClockId == metadata.clockId info.provider = null } @@ -197,10 +198,11 @@ open class ClockRegistry( if (manager != info.manager) { logger.e({ "Clock Id conflict on load: " + - "$str1 is double registered by $str2 and $str3" + "$str1 is double registered by $str2 and $str3. " + + "Using $str2 since it was attached first." }) { str1 = id - str2 = info.manager.toString() + str2 = info.manager?.toString() ?: info.provider?.toString() str3 = manager.toString() } manager.unloadPlugin() @@ -227,10 +229,11 @@ open class ClockRegistry( if (info?.manager != manager) { logger.e({ "Clock Id conflict on unload: " + - "$str1 is double registered by $str2 and $str3" + "$str1 is double registered by $str2 and $str3. " + + "Using $str2 since it was attached first." }) { str1 = id - str2 = info?.manager.toString() + str2 = info?.manager?.toString() ?: info?.provider?.toString() str3 = manager.toString() } continue diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt index 9fb60c75b046..98cf68468151 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt @@ -47,7 +47,7 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) : init { fun createController(cfg: LayerConfig) { - val controller = SimpleDigitalHandLayerController(clockCtx, cfg) + val controller = SimpleDigitalHandLayerController(clockCtx, cfg, isLargeClock = true) view.addView(controller.view) layerControllers.add(controller) } @@ -55,31 +55,20 @@ class ComposedDigitalLayerController(private val clockCtx: ClockContext) : val layerCfg = LayerConfig( style = FontTextStyle(lineHeight = 147.25f), + timespec = DigitalTimespec.DIGIT_PAIR, + alignment = DigitalAlignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER), aodStyle = FontTextStyle( transitionInterpolator = Interpolators.EMPHASIZED, transitionDuration = 750, ), - alignment = - DigitalAlignment(HorizontalAlignment.CENTER, VerticalAlignment.BASELINE), - // Placeholders - timespec = DigitalTimespec.TIME_FULL_FORMAT, + // Placeholder dateTimeFormat = "hh:mm", ) - createController( - layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "hh") - ) - createController( - layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "hh") - ) - createController( - layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "mm") - ) - createController( - layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "mm") - ) + createController(layerCfg.copy(dateTimeFormat = "hh")) + createController(layerCfg.copy(dateTimeFormat = "mm")) } private fun refreshTime() { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index 6cc281ace481..af9f2ce9d73f 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -17,21 +17,21 @@ import android.content.Context import android.content.res.Resources import android.graphics.Typeface import android.view.LayoutInflater -import com.android.systemui.animation.GSFAxes import com.android.systemui.customization.R import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.ClockController -import com.android.systemui.plugins.clocks.ClockFontAxis -import com.android.systemui.plugins.clocks.ClockFontAxisSetting +import com.android.systemui.plugins.clocks.ClockFontAxis.Companion.merge import com.android.systemui.plugins.clocks.ClockLogger import com.android.systemui.plugins.clocks.ClockMessageBuffers import com.android.systemui.plugins.clocks.ClockMetadata import com.android.systemui.plugins.clocks.ClockPickerConfig import com.android.systemui.plugins.clocks.ClockProvider import com.android.systemui.plugins.clocks.ClockSettings +import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes private val TAG = DefaultClockProvider::class.simpleName const val DEFAULT_CLOCK_ID = "DEFAULT" +const val FLEX_CLOCK_ID = "DIGITAL_CLOCK_FLEX" data class ClockContext( val context: Context, @@ -55,16 +55,20 @@ class DefaultClockProvider( messageBuffers = buffers } - override fun getClocks(): List<ClockMetadata> = listOf(ClockMetadata(DEFAULT_CLOCK_ID)) + override fun getClocks(): List<ClockMetadata> { + var clocks = listOf(ClockMetadata(DEFAULT_CLOCK_ID)) + if (isClockReactiveVariantsEnabled) clocks += ClockMetadata(FLEX_CLOCK_ID) + return clocks + } override fun createClock(settings: ClockSettings): ClockController { - if (settings.clockId != DEFAULT_CLOCK_ID) { + if (getClocks().all { it.clockId != settings.clockId }) { throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG") } return if (isClockReactiveVariantsEnabled) { val buffers = messageBuffers ?: ClockMessageBuffers(ClockLogger.DEFAULT_MESSAGE_BUFFER) - val fontAxes = ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes) + val fontAxes = getDefaultAxes(settings).merge(settings.axes) val clockSettings = settings.copy(axes = fontAxes.map { it.toSetting() }) val typefaceCache = TypefaceCache(buffers.infraMessageBuffer, NUM_CLOCK_FONT_ANIMATION_STEPS) { @@ -86,15 +90,15 @@ class DefaultClockProvider( } override fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig { - if (settings.clockId != DEFAULT_CLOCK_ID) { + if (getClocks().all { it.clockId != settings.clockId }) { throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG") } val fontAxes = if (!isClockReactiveVariantsEnabled) listOf() - else ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes) + else getDefaultAxes(settings).merge(settings.axes) return ClockPickerConfig( - DEFAULT_CLOCK_ID, + settings.clockId ?: DEFAULT_CLOCK_ID, resources.getString(R.string.clock_default_name), resources.getString(R.string.clock_default_description), resources.getDrawable(R.drawable.clock_default_thumbnail, null), @@ -106,23 +110,6 @@ class DefaultClockProvider( companion object { const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30 - // TODO(b/364681643): Variations for retargetted DIGITAL_CLOCK_FLEX - val LEGACY_FLEX_LS_VARIATION = - listOf( - ClockFontAxisSetting(GSFAxes.WEIGHT, 600f), - ClockFontAxisSetting(GSFAxes.WIDTH, 100f), - ClockFontAxisSetting(GSFAxes.ROUND, 100f), - ClockFontAxisSetting(GSFAxes.SLANT, 0f), - ) - - val LEGACY_FLEX_AOD_VARIATION = - listOf( - ClockFontAxisSetting(GSFAxes.WEIGHT, 74f), - ClockFontAxisSetting(GSFAxes.WIDTH, 43f), - ClockFontAxisSetting(GSFAxes.ROUND, 100f), - ClockFontAxisSetting(GSFAxes.SLANT, 0f), - ) - val FLEX_TYPEFACE by lazy { // TODO(b/364680873): Move constant to config_clockFontFamily when shipping Typeface.create("google-sans-flex-clock", Typeface.NORMAL) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index cc3769e0a568..004d1aa1fe93 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -24,7 +24,9 @@ import com.android.systemui.plugins.clocks.ClockConfig import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFontAxis +import com.android.systemui.plugins.clocks.ClockFontAxis.Companion.merge import com.android.systemui.plugins.clocks.ClockFontAxisSetting +import com.android.systemui.plugins.clocks.ClockSettings import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.shared.clocks.view.FlexClockView @@ -94,7 +96,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController } override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) { - val fontAxes = ClockFontAxis.merge(FONT_AXES, axes).map { it.toSetting() } + val fontAxes = getDefaultAxes(clockCtx.settings).merge(axes).map { it.toSetting() } smallClock.events.onFontAxesChanged(fontAxes) largeClock.events.onFontAxesChanged(fontAxes) } @@ -120,7 +122,13 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController override fun dump(pw: PrintWriter) {} companion object { - val FONT_AXES = + fun getDefaultAxes(settings: ClockSettings): List<ClockFontAxis> { + return if (settings.clockId == FLEX_CLOCK_ID) { + FONT_AXES.merge(LEGACY_FLEX_SETTINGS) + } else FONT_AXES + } + + private val FONT_AXES = listOf( ClockFontAxis( key = GSFAxes.WEIGHT, @@ -135,7 +143,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController key = GSFAxes.WIDTH, type = AxisType.Float, minValue = 25f, - currentValue = 100f, + currentValue = 85f, maxValue = 151f, name = "Width", description = "Glyph Width", @@ -159,5 +167,13 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController description = "Glyph Slant", ), ) + + private val LEGACY_FLEX_SETTINGS = + listOf( + ClockFontAxisSetting(GSFAxes.WEIGHT, 600f), + ClockFontAxisSetting(GSFAxes.WIDTH, 100f), + ClockFontAxisSetting(GSFAxes.ROUND, 100f), + ClockFontAxisSetting(GSFAxes.SLANT, 0f), + ) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index d7d8d28a71e0..cfcf201796da 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -60,7 +60,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: init { layerController = if (isLargeClock) ComposedDigitalLayerController(clockCtx) - else SimpleDigitalHandLayerController(clockCtx, SMALL_LAYER_CONFIG) + else SimpleDigitalHandLayerController(clockCtx, SMALL_LAYER_CONFIG, isLargeClock) layerController.view.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).apply { gravity = Gravity.CENTER } @@ -148,21 +148,6 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: * keyguard_large_clock_top_margin from default clock */ override fun onTargetRegionChanged(targetRegion: Rect?) { - // When a clock needs to be aligned with screen, like weather clock - // it needs to offset back the translation of keyguard_large_clock_top_margin - if (isLargeClock && (view as FlexClockView).isAlignedWithScreen()) { - val topMargin = keyguardLargeClockTopMargin - targetRegion?.let { - val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock) - // In LS, we use yDiff to counter translate - // the translation of KeyguardLargeClockTopMargin - // With the targetRegion passed from picker, - // we will have yDiff = 0, no translation is needed for weather clock - if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2 - } - return - } - var maxWidth = 0f var maxHeight = 0f @@ -231,7 +216,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: } override fun onPickerCarouselSwiping(swipingFraction: Float) { - if (isLargeClock && !(view as FlexClockView).isAlignedWithScreen()) { + if (isLargeClock) { view.translationY = keyguardLargeClockTopMargin / 2F * swipingFraction } layerController.animations.onPickerCarouselSwiping(swipingFraction) @@ -251,6 +236,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: companion object { val SMALL_CLOCK_MAX_WDTH = 120f + val SMALL_LAYER_CONFIG = LayerConfig( timespec = DigitalTimespec.TIME_FULL_FORMAT, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt index 82fc35012dbc..1659814b74eb 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -71,6 +71,7 @@ data class FontTextStyle( enum class DigitalTimespec { TIME_FULL_FORMAT, + DIGIT_PAIR, FIRST_DIGIT, SECOND_DIGIT, } @@ -78,8 +79,9 @@ enum class DigitalTimespec { open class SimpleDigitalHandLayerController( private val clockCtx: ClockContext, private val layerCfg: LayerConfig, + isLargeClock: Boolean, ) : SimpleClockLayerController { - override val view = SimpleDigitalClockTextView(clockCtx) + override val view = SimpleDigitalClockTextView(clockCtx, isLargeClock) private val logger = Logger(clockCtx.messageBuffer, TAG) val timespec = DigitalTimespecHandler(layerCfg.timespec, layerCfg.dateTimeFormat) @@ -120,6 +122,28 @@ open class SimpleDigitalHandLayerController( } } + private fun applyLayout() { + // TODO: Remove NO-OP + if (view.layoutParams is RelativeLayout.LayoutParams) { + val lp = view.layoutParams as RelativeLayout.LayoutParams + lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER) + when (view.id) { + R.id.HOUR_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.ALIGN_PARENT_START) + } + R.id.MINUTE_DIGIT_PAIR -> { + lp.addRule(RelativeLayout.CENTER_VERTICAL) + lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR) + } + else -> { + throw Exception("cannot apply two pairs layout to view ${view.id}") + } + } + view.layoutParams = lp + } + } + override val events = object : ClockEvents { override var isReactiveTouchInteractionEnabled = false @@ -154,6 +178,7 @@ open class SimpleDigitalHandLayerController( override val animations = object : ClockAnimations { override fun enter() { + applyLayout() refreshTime() } @@ -169,6 +194,7 @@ open class SimpleDigitalHandLayerController( } override fun fold(fraction: Float) { + applyLayout() refreshTime() } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt index 37db783aba53..8b3b92921ee0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt @@ -106,19 +106,16 @@ class DigitalTimespecHandler( ) } - private fun getSingleDigit(): String { - val isFirstDigit = timespec == DigitalTimespec.FIRST_DIGIT + private fun getSingleDigit(offset: Int): String { val text = dateFormat.format(cal.time).toString() - return text.substring( - if (isFirstDigit) 0 else text.length - 1, - if (isFirstDigit) text.length - 1 else text.length, - ) + return text.substring(offset, offset + 1) } fun getDigitString(): String { return when (timespec) { - DigitalTimespec.FIRST_DIGIT, - DigitalTimespec.SECOND_DIGIT -> getSingleDigit() + DigitalTimespec.FIRST_DIGIT -> getSingleDigit(0) + DigitalTimespec.SECOND_DIGIT -> getSingleDigit(1) + DigitalTimespec.DIGIT_PAIR -> dateFormat.format(cal.time).toString() DigitalTimespec.TIME_FULL_FORMAT -> dateFormat.format(cal.time).toString() } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index 55750b5e0925..f0f344a605a9 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -87,7 +87,7 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { protected fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? { maxSingleDigitSize = Point(-1, -1) - val bottomLocation: (textView: SimpleDigitalClockTextView) -> Int = { textView -> + val viewHeight: (textView: SimpleDigitalClockTextView) -> Int = { textView -> if (isMonoVerticalNumericLineSpacing) { maxSingleDigitSize.y } else { @@ -98,9 +98,15 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { digitalClockTextViewMap.forEach { (_, textView) -> textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) maxSingleDigitSize.x = max(maxSingleDigitSize.x, textView.measuredWidth) - maxSingleDigitSize.y = max(bottomLocation(textView), textView.measuredHeight) + maxSingleDigitSize.y = max(viewHeight(textView), textView.measuredHeight) } aodTranslate = Point(0, 0) + // TODO(b/364680879): Cleanup + /* + aodTranslate = Point( + (maxSingleDigitSize.x * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt(), + (maxSingleDigitSize.y * AOD_VERTICAL_TRANSLATE_RATIO).toInt()) + */ return Point( ((maxSingleDigitSize.x + abs(aodTranslate.x)) * 2), ((maxSingleDigitSize.y + abs(aodTranslate.y)) * 2), @@ -112,6 +118,10 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0) digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y) digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitSize) + digitLeftTopMap[R.id.HOUR_DIGIT_PAIR] = Point(maxSingleDigitSize.x / 2, 0) + // Add a small vertical buffer for the second digit pair + digitLeftTopMap[R.id.MINUTE_DIGIT_PAIR] = + Point(maxSingleDigitSize.x / 2, (maxSingleDigitSize.y * 1.05f).toInt()) digitLeftTopMap.forEach { (_, point) -> point.x += abs(aodTranslate.x) point.y += abs(aodTranslate.y) @@ -179,9 +189,9 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { // save canvas location in anticipation of restoration later canvas.save() val xTranslateAmount = - digitOffsets.getOrDefault(id, 0f) + digitLeftTopMap[id]!!.x.toFloat() + digitOffsets.getOrDefault(id, 0f) + (digitLeftTopMap[id]?.x?.toFloat() ?: 0f) // move canvas to location that the textView would like - canvas.translate(xTranslateAmount, digitLeftTopMap[id]!!.y.toFloat()) + canvas.translate(xTranslateAmount, digitLeftTopMap[id]?.y?.toFloat() ?: 0f) // draw the textView at the location of the canvas above textView.draw(canvas) // reset the canvas location back to 0 without drawing @@ -189,8 +199,6 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { } } - fun isAlignedWithScreen(): Boolean = false - fun onLocaleChanged(locale: Locale) { updateLocale(locale) requestLayout() @@ -302,23 +310,17 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { clockMoveDirection: Int, moveFraction: Float, ) { + // TODO(b/393577936): The step animation isn't correct with the two pairs approach val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0 // The sign of moveAmountDeltaForDigit is already set here // we can interpret (left - clockStartLeft) as (destinationPosition - originPosition) // so we no longer need to multiply direct sign to moveAmountDeltaForDigit val currentMoveAmount = left - clockStartLeft - for (i in 0 until NUM_DIGITS) { - val mapIndexToId = - when (i) { - 0 -> R.id.HOUR_FIRST_DIGIT - 1 -> R.id.HOUR_SECOND_DIGIT - 2 -> R.id.MINUTE_FIRST_DIGIT - 3 -> R.id.MINUTE_SECOND_DIGIT - else -> -1 - } + var index = 0 + digitalClockTextViewMap.forEach { id, _ -> val digitFraction = getDigitFraction( - digit = i, + digit = index++, isMovingToCenter = isMovingToCenter, fraction = moveFraction, ) @@ -326,7 +328,7 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { val moveAmountForDigit = currentMoveAmount * digitFraction var moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount if (isMovingToCenter && moveAmountForDigit < 0) moveAmountDeltaForDigit *= -1 - digitOffsets[mapIndexToId] = moveAmountDeltaForDigit + digitOffsets[id] = moveAmountDeltaForDigit invalidate() } } @@ -347,7 +349,8 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { /* rangeMin= */ 0.0f, /* rangeMax= */ 1.0f, /* valueMin= */ digitInitialDelay, - /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME, + /* valueMax= */ digitInitialDelay + + availableAnimationTime(digitalClockTextViewMap.size), /* value= */ fraction, ) ) @@ -357,12 +360,8 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { val AOD_TRANSITION_DURATION = 750L val CHARGING_TRANSITION_DURATION = 300L - // Calculate the positions of all of the digits... - // Offset each digit by, say, 0.1 - // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should - // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3 - // from 0.3 - 1.0. - private const val NUM_DIGITS = 4 + val AOD_HORIZONTAL_TRANSLATE_RATIO = -0.15F + val AOD_VERTICAL_TRANSLATE_RATIO = 0.075F // Delays. Each digit's animation should have a slight delay, so we get a nice // "stepping" effect. When moving right, the second digit of the hour should move first. @@ -387,7 +386,9 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { // Total available transition time for each digit, taking into account the step. If step is // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7. - private const val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1) + private fun availableAnimationTime(numDigits: Int): Float { + return 1.0f - MOVE_DIGIT_STEP * (numDigits.toFloat() - 1) + } // Add language tags below that do not have vertically mono spaced numerals private val NON_MONO_VERTICAL_NUMERIC_LINE_SPACING_LANGUAGES = @@ -415,6 +416,14 @@ class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { outPoint.x *= 1 outPoint.y *= 1 } + R.id.HOUR_DIGIT_PAIR -> { + outPoint.x *= -1 + outPoint.y *= -1 + } + R.id.MINUTE_DIGIT_PAIR -> { + outPoint.x *= -1 + outPoint.y *= 1 + } } return outPoint } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index db39162205b2..13f563389e19 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -38,10 +38,13 @@ import com.android.systemui.animation.GSFAxes import com.android.systemui.animation.TextAnimator import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.ClockFontAxisSetting +import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace +import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar import com.android.systemui.plugins.clocks.ClockLogger import com.android.systemui.shared.clocks.ClockContext import com.android.systemui.shared.clocks.DigitTranslateAnimator import com.android.systemui.shared.clocks.DimensionParser +import com.android.systemui.shared.clocks.FLEX_CLOCK_ID import com.android.systemui.shared.clocks.FontTextStyle import java.lang.Thread import kotlin.math.max @@ -63,14 +66,32 @@ enum class HorizontalAlignment { } @SuppressLint("AppCompatCustomView") -open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSet? = null) : - TextView(clockCtx.context, attrs) { +open class SimpleDigitalClockTextView( + clockCtx: ClockContext, + isLargeClock: Boolean, + attrs: AttributeSet? = null, +) : TextView(clockCtx.context, attrs) { val lockScreenPaint = TextPaint() lateinit var textStyle: FontTextStyle lateinit var aodStyle: FontTextStyle - private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION) - private var aodFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_AOD_VARIATION) + private val isLegacyFlex = clockCtx.settings.clockId == FLEX_CLOCK_ID + private val fixedAodAxes = + when { + !isLegacyFlex -> listOf(AOD_WEIGHT_AXIS, WIDTH_AXIS) + isLargeClock -> listOf(FLEX_AOD_LARGE_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS) + else -> listOf(FLEX_AOD_SMALL_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS) + } + + private var lsFontVariation = + if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS).toFVar() + else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS).toFVar() + + private var aodFontVariation = run { + val roundAxis = if (!isLegacyFlex) ROUND_AXIS else FLEX_ROUND_AXIS + (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar() + } + private val parser = DimensionParser(clockCtx.context) var maxSingleDigitHeight = -1 var maxSingleDigitWidth = -1 @@ -129,8 +150,14 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe invalidate() } - fun updateAxes(axes: List<ClockFontAxisSetting>) { - lsFontVariation = ClockFontAxisSetting.toFVar(axes + OPTICAL_SIZE_AXIS) + fun updateAxes(lsAxes: List<ClockFontAxisSetting>) { + lsFontVariation = lsAxes.toFVar() + aodFontVariation = lsAxes.replace(fixedAodAxes).toFVar() + logger.i({ "updateAxes(LS = $str1, AOD = $str2)" }) { + str1 = lsFontVariation + str2 = aodFontVariation + } + lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) typeface = lockScreenPaint.typeface @@ -287,6 +314,7 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe targetTextBounds, ) } + if (layout == null) { requestLayout() } else { @@ -501,22 +529,18 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) } val AOD_COLOR = Color.WHITE - val OPTICAL_SIZE_AXIS = ClockFontAxisSetting(GSFAxes.OPTICAL_SIZE, 144f) - val DEFAULT_LS_VARIATION = - listOf( - OPTICAL_SIZE_AXIS, - ClockFontAxisSetting(GSFAxes.WEIGHT, 400f), - ClockFontAxisSetting(GSFAxes.WIDTH, 100f), - ClockFontAxisSetting(GSFAxes.ROUND, 0f), - ClockFontAxisSetting(GSFAxes.SLANT, 0f), - ) - val DEFAULT_AOD_VARIATION = - listOf( - OPTICAL_SIZE_AXIS, - ClockFontAxisSetting(GSFAxes.WEIGHT, 200f), - ClockFontAxisSetting(GSFAxes.WIDTH, 100f), - ClockFontAxisSetting(GSFAxes.ROUND, 0f), - ClockFontAxisSetting(GSFAxes.SLANT, 0f), - ) + val LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 400f) + val AOD_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 200f) + val WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 85f) + val ROUND_AXIS = ClockFontAxisSetting(GSFAxes.ROUND, 0f) + val SLANT_AXIS = ClockFontAxisSetting(GSFAxes.SLANT, 0f) + + // Axes for Legacy version of the Flex Clock + val FLEX_LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 600f) + val FLEX_AOD_LARGE_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 74f) + val FLEX_AOD_SMALL_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 133f) + val FLEX_LS_WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 100f) + val FLEX_AOD_WIDTH_AXIS = ClockFontAxisSetting(GSFAxes.WIDTH, 43f) + val FLEX_ROUND_AXIS = ClockFontAxisSetting(GSFAxes.ROUND, 100f) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index 99146f257c12..bd0fb68a9c42 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -25,8 +25,10 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository import com.android.systemui.keyguard.shared.model.ClockSize +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn @@ -236,6 +238,10 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa val isContentVisible by collectLastValue(underTest.isContentVisible) keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null) + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.OCCLUDED, + ) runCurrent() assertThat(isContentVisible).isFalse() } @@ -246,12 +252,46 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa with(kosmos) { testScope.runTest { val isContentVisible by collectLastValue(underTest.isContentVisible) + + keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null) + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.OCCLUDED, + ) + runCurrent() + + sceneInteractor.snapToScene(Scenes.Shade, "") + runCurrent() + assertThat(isContentVisible).isFalse() + } + } + + @Test + fun isContentVisible_whenOccluded_notVisibleInOccluded_visibleInAod() = + with(kosmos) { + testScope.runTest { + val isContentVisible by collectLastValue(underTest.isContentVisible) keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null) + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.LOCKSCREEN, + KeyguardState.OCCLUDED, + ) runCurrent() sceneInteractor.snapToScene(Scenes.Shade, "") runCurrent() assertThat(isContentVisible).isFalse() + + fakeKeyguardTransitionRepository.transitionTo( + KeyguardState.OCCLUDED, + KeyguardState.AOD, + ) + runCurrent() + + sceneInteractor.snapToScene(Scenes.Lockscreen, "") + runCurrent() + + assertThat(isContentVisible).isTrue() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt index 2a4cba8cffe2..41d7e490ddc2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt @@ -41,7 +41,6 @@ import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.res.R -import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.connectivity.AccessPointController import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository @@ -145,7 +144,7 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { dialogManager, wifiStateWorker, accessPointController, - internetDetailsViewModelFactory + internetDetailsViewModelFactory, ) underTest.initialize() @@ -295,7 +294,6 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { FLAG_SCENE_CONTAINER, KeyguardWmStateRefactor.FLAG_NAME, NotificationThrottleHun.FLAG_NAME, - DualShade.FLAG_NAME, ] ) fun click_withQsDetailedViewEnabled() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index a79f4085ec6d..d2ea62da0940 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -29,10 +29,12 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shared.Flags as SharedFlags import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScrimController @@ -80,6 +82,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Mock private lateinit var blurUtils: BlurUtils @Mock private lateinit var biometricUnlockController: BiometricUnlockController @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var keyguardInteractor: KeyguardInteractor @Mock private lateinit var choreographer: Choreographer @Mock private lateinit var wallpaperController: WallpaperController @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController @@ -123,6 +126,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { blurUtils, biometricUnlockController, keyguardStateController, + keyguardInteractor, choreographer, wallpaperController, notificationShadeWindowController, @@ -308,6 +312,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test + @DisableFlags(SharedFlags.FLAG_AMBIENT_AOD) fun onDozeAmountChanged_appliesBlur() { statusBarStateListener.onDozeAmountChanged(1f, 1f) notificationShadeDepthController.updateBlurCallback.doFrame(0) @@ -315,6 +320,14 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { } @Test + @EnableFlags(SharedFlags.FLAG_AMBIENT_AOD) + fun onDozeAmountChanged_doesNotApplyBlurWithAmbientAod() { + statusBarStateListener.onDozeAmountChanged(1f, 1f) + notificationShadeDepthController.updateBlurCallback.doFrame(0) + verify(blurUtils).applyBlur(any(), eq(0), eq(false)) + } + + @Test fun setFullShadeTransition_appliesBlur_onlyIfSupported() { reset(blurUtils) `when`(blurUtils.blurRadiusOfRatio(anyFloat())).then { answer -> diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt index 04a8b5850f37..ee4d0990d38f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt @@ -15,6 +15,7 @@ package com.android.systemui.statusbar.notification.icon.domain.interactor +import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -24,6 +25,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.data.repository.notificationListenerSettingsRepository +import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -62,7 +64,7 @@ class NotificationIconsInteractorTest : SysuiTestCase() { kosmos.activeNotificationsInteractor, kosmos.bubblesOptional, kosmos.headsUpNotificationIconInteractor, - kosmos.notificationsKeyguardViewStateRepository + kosmos.notificationsKeyguardViewStateRepository, ) @Before @@ -306,6 +308,7 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { } @Test + @DisableFlags(StatusBarNoHunBehavior.FLAG_NAME) fun filteredEntrySet_includesIsolatedIcon() = testScope.runTest { val filteredSet by collectLastValue(underTest.statusBarNotifs) @@ -316,31 +319,11 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { private val testIcons = listOf( - activeNotificationModel( - key = "notif1", - ), - activeNotificationModel( - key = "notif2", - isAmbient = true, - ), - activeNotificationModel( - key = "notif3", - isRowDismissed = true, - ), - activeNotificationModel( - key = "notif4", - isSilent = true, - ), - activeNotificationModel( - key = "notif5", - isLastMessageFromReply = true, - ), - activeNotificationModel( - key = "notif6", - isSuppressedFromStatusBar = true, - ), - activeNotificationModel( - key = "notif7", - isPulsing = true, - ), + activeNotificationModel(key = "notif1"), + activeNotificationModel(key = "notif2", isAmbient = true), + activeNotificationModel(key = "notif3", isRowDismissed = true), + activeNotificationModel(key = "notif4", isSilent = true), + activeNotificationModel(key = "notif5", isLastMessageFromReply = true), + activeNotificationModel(key = "notif6", isSuppressedFromStatusBar = true), + activeNotificationModel(key = "notif7", isPulsing = true), ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt index 739636e01f0b..d14ff35f824a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.stack import android.os.testableLooper import android.testing.TestableLooper.RunWithLooper +import androidx.dynamicanimation.animation.SpringForce import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -46,13 +47,14 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { private val childrenNumber = 5 private val stackScrollLayout = mock<NotificationStackScrollLayout>() private val sectionsManager = mock<NotificationSectionsManager>() - private val swipedMultiplier = 0.5f private val msdlPlayer = kosmos.fakeMSDLPlayer + private var canRowBeDismissed = true private val underTest = kosmos.magneticNotificationRowManagerImpl private lateinit var notificationTestHelper: NotificationTestHelper private lateinit var children: NotificationChildrenContainer + private lateinit var swipedRow: ExpandableNotificationRow @Before fun setUp() { @@ -60,14 +62,15 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { notificationTestHelper = NotificationTestHelper(mContext, mDependency, kosmos.testableLooper, featureFlags) children = notificationTestHelper.createGroup(childrenNumber).childrenContainer + swipedRow = children.attachedChildren[childrenNumber / 2] + configureMagneticRowListener(swipedRow) } @Test fun setMagneticAndRoundableTargets_onIdle_targetsGetSet() = kosmos.testScope.runTest { // WHEN the targets are set for a row - val row = children.attachedChildren[childrenNumber / 2] - setTargetsForRow(row) + setTargets() // THEN the magnetic and roundable targets are defined and the state is TARGETS_SET assertThat(underTest.currentState).isEqualTo(State.TARGETS_SET) @@ -79,11 +82,10 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { fun setMagneticRowTranslation_whenTargetsAreSet_startsPulling() = kosmos.testScope.runTest { // GIVEN targets are set - val row = children.attachedChildren[childrenNumber / 2] - setTargetsForRow(row) + setTargets() // WHEN setting a translation for the swiped row - underTest.setMagneticRowTranslation(row, translation = 100f) + underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // THEN the state moves to PULLING assertThat(underTest.currentState).isEqualTo(State.PULLING) @@ -107,8 +109,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { fun setMagneticRowTranslation_whenRowIsNotSwiped_doesNotSetMagneticTranslation() = kosmos.testScope.runTest { // GIVEN that targets are set - val row = children.attachedChildren[childrenNumber / 2] - setTargetsForRow(row) + setTargets() // WHEN setting a translation for a row that is not being swiped val differentRow = children.attachedChildren[childrenNumber / 2 - 1] @@ -120,41 +121,61 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { } @Test - fun setMagneticRowTranslation_belowThreshold_whilePulling_setsMagneticTranslations() = + fun setMagneticRowTranslation_whenDismissible_belowThreshold_whenPulling_setsTranslations() = kosmos.testScope.runTest { // GIVEN a threshold of 100 px val threshold = 100f underTest.setSwipeThresholdPx(threshold) // GIVEN that targets are set and the rows are being pulled - val row = children.attachedChildren[childrenNumber / 2] - setTargetsForRow(row) - underTest.setMagneticRowTranslation(row, translation = 100f) + setTargets() + underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // WHEN setting a translation that will fall below the threshold - val translation = threshold / swipedMultiplier - 50f - underTest.setMagneticRowTranslation(row, translation) + val translation = threshold / underTest.swipedRowMultiplier - 50f + underTest.setMagneticRowTranslation(swipedRow, translation) // THEN the targets continue to be pulled and translations are set assertThat(underTest.currentState).isEqualTo(State.PULLING) - assertThat(row.translation).isEqualTo(swipedMultiplier * translation) + assertThat(swipedRow.translation).isEqualTo(underTest.swipedRowMultiplier * translation) } @Test - fun setMagneticRowTranslation_aboveThreshold_whilePulling_detachesMagneticTargets() = + fun setMagneticRowTranslation_whenNotDismissible_belowThreshold_whenPulling_setsTranslations() = kosmos.testScope.runTest { // GIVEN a threshold of 100 px val threshold = 100f underTest.setSwipeThresholdPx(threshold) // GIVEN that targets are set and the rows are being pulled - val row = children.attachedChildren[childrenNumber / 2] - setTargetsForRow(row) - underTest.setMagneticRowTranslation(row, translation = 100f) + canRowBeDismissed = false + setTargets() + underTest.setMagneticRowTranslation(swipedRow, translation = 100f) + + // WHEN setting a translation that will fall below the threshold + val translation = threshold / underTest.swipedRowMultiplier - 50f + underTest.setMagneticRowTranslation(swipedRow, translation) + + // THEN the targets continue to be pulled and reduced translations are set + val expectedTranslation = getReducedTranslation(translation) + assertThat(underTest.currentState).isEqualTo(State.PULLING) + assertThat(swipedRow.translation).isEqualTo(expectedTranslation) + } + + @Test + fun setMagneticRowTranslation_whenDismissible_aboveThreshold_whilePulling_detaches() = + kosmos.testScope.runTest { + // GIVEN a threshold of 100 px + val threshold = 100f + underTest.setSwipeThresholdPx(threshold) + + // GIVEN that targets are set and the rows are being pulled + setTargets() + underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // WHEN setting a translation that will fall above the threshold - val translation = threshold / swipedMultiplier + 50f - underTest.setMagneticRowTranslation(row, translation) + val translation = threshold / underTest.swipedRowMultiplier + 50f + underTest.setMagneticRowTranslation(swipedRow, translation) // THEN the swiped view detaches and the correct detach haptics play assertThat(underTest.currentState).isEqualTo(State.DETACHED) @@ -162,15 +183,36 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { } @Test + fun setMagneticRowTranslation_whenNotDismissible_aboveThreshold_whilePulling_doesNotDetach() = + kosmos.testScope.runTest { + // GIVEN a threshold of 100 px + val threshold = 100f + underTest.setSwipeThresholdPx(threshold) + + // GIVEN that targets are set and the rows are being pulled + canRowBeDismissed = false + setTargets() + underTest.setMagneticRowTranslation(swipedRow, translation = 100f) + + // WHEN setting a translation that will fall above the threshold + val translation = threshold / underTest.swipedRowMultiplier + 50f + underTest.setMagneticRowTranslation(swipedRow, translation) + + // THEN the swiped view does not detach and the reduced translation is set + val expectedTranslation = getReducedTranslation(translation) + assertThat(underTest.currentState).isEqualTo(State.PULLING) + assertThat(swipedRow.translation).isEqualTo(expectedTranslation) + } + + @Test fun setMagneticRowTranslation_whileDetached_setsTranslationAndStaysDetached() = kosmos.testScope.runTest { // GIVEN that the swiped view has been detached - val row = children.attachedChildren[childrenNumber / 2] - setDetachedState(row) + setDetachedState() // WHEN setting a new translation val translation = 300f - underTest.setMagneticRowTranslation(row, translation) + underTest.setMagneticRowTranslation(swipedRow, translation) // THEN the swiped view continues to be detached assertThat(underTest.currentState).isEqualTo(State.DETACHED) @@ -180,14 +222,13 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { fun onMagneticInteractionEnd_whilePulling_goesToIdle() = kosmos.testScope.runTest { // GIVEN targets are set - val row = children.attachedChildren[childrenNumber / 2] - setTargetsForRow(row) + setTargets() // WHEN setting a translation for the swiped row - underTest.setMagneticRowTranslation(row, translation = 100f) + underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // WHEN the interaction ends on the row - underTest.onMagneticInteractionEnd(row, velocity = null) + underTest.onMagneticInteractionEnd(swipedRow, velocity = null) // THEN the state resets assertThat(underTest.currentState).isEqualTo(State.IDLE) @@ -197,32 +238,56 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { fun onMagneticInteractionEnd_whileDetached_goesToIdle() = kosmos.testScope.runTest { // GIVEN the swiped row is detached - val row = children.attachedChildren[childrenNumber / 2] - setDetachedState(row) + setDetachedState() // WHEN the interaction ends on the row - underTest.onMagneticInteractionEnd(row, velocity = null) + underTest.onMagneticInteractionEnd(swipedRow, velocity = null) // THEN the state resets assertThat(underTest.currentState).isEqualTo(State.IDLE) } - private fun setDetachedState(row: ExpandableNotificationRow) { + private fun setDetachedState() { val threshold = 100f underTest.setSwipeThresholdPx(threshold) // Set the pulling state - setTargetsForRow(row) - underTest.setMagneticRowTranslation(row, translation = 100f) + setTargets() + underTest.setMagneticRowTranslation(swipedRow, translation = 100f) // Set a translation that will fall above the threshold - val translation = threshold / swipedMultiplier + 50f - underTest.setMagneticRowTranslation(row, translation) + val translation = threshold / underTest.swipedRowMultiplier + 50f + underTest.setMagneticRowTranslation(swipedRow, translation) assertThat(underTest.currentState).isEqualTo(State.DETACHED) } - private fun setTargetsForRow(row: ExpandableNotificationRow) { - underTest.setMagneticAndRoundableTargets(row, stackScrollLayout, sectionsManager) + private fun setTargets() { + underTest.setMagneticAndRoundableTargets(swipedRow, stackScrollLayout, sectionsManager) + } + + private fun getReducedTranslation(originalTranslation: Float) = + underTest.swipedRowMultiplier * + originalTranslation * + MagneticNotificationRowManagerImpl.MAGNETIC_REDUCTION + + private fun configureMagneticRowListener(row: ExpandableNotificationRow) { + val listener = + object : MagneticRowListener { + override fun setMagneticTranslation(translation: Float) { + row.translation = translation + } + + override fun triggerMagneticForce( + endTranslation: Float, + springForce: SpringForce, + startVelocity: Float, + ) {} + + override fun cancelMagneticAnimations() {} + + override fun canRowBeDismissed(): Boolean = canRowBeDismissed + } + row.magneticRowListener = listener } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt index 12d122b2fe1d..80e9e36862dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt @@ -170,6 +170,41 @@ class NotificationStatsLoggerTest : SysuiTestCase() { } @Test + fun onNotificationVisibilityChanged_thenShadeNotInteractive_noDuplicateLogs() = + testScope.runTest { + // GIVEN a visible Notifications is reported + val (ranks, locations) = fakeNotificationMaps("key0") + val callable = Callable { locations } + underTest.onNotificationLocationsChanged(callable, ranks) + runCurrent() + clearInvocations(mockStatusBarService, mockNotificationListenerService) + + // WHEN the same Notification becomins invisible + val emptyCallable = Callable { emptyMap<String, Int>() } + underTest.onNotificationLocationsChanged(emptyCallable, ranks) + // AND notifications become non interactible + underTest.onLockscreenOrShadeNotInteractive(emptyList()) + runCurrent() + + // THEN visibility changes are reported + verify(mockStatusBarService) + .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture()) + val noLongerVisible = visibilityArrayCaptor.value + assertThat(noLongerVisible).hasLength(1) + assertThat(noLongerVisible[0]).apply { + isKeyEqualTo("key0") + isRankEqualTo(0) + notVisible() + isInMainArea() + isCountEqualTo(1) + } + + // AND nothing else is logged + verifyNoMoreInteractions(mockStatusBarService) + verifyNoMoreInteractions(mockNotificationListenerService) + } + + @Test fun onNotificationListUpdated_itemsChangedPositions_nothingLogged() = testScope.runTest { // GIVEN some visible Notifications are reported @@ -253,14 +288,14 @@ class NotificationStatsLoggerTest : SysuiTestCase() { activeNotificationModel( key = "key0", uid = 0, - packageName = "com.android.first" + packageName = "com.android.first", ), activeNotificationModel( key = "key1", uid = 1, - packageName = "com.android.second" + packageName = "com.android.second", ), - ) + ), ) runCurrent() @@ -286,7 +321,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = true, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() @@ -296,7 +331,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { /* key = */ "key", /* userAction = */ true, /* expanded = */ true, - NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, ) } @@ -308,7 +343,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = true, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() clearInvocations(mockStatusBarService) @@ -318,7 +353,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = true, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() @@ -334,7 +369,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = true, location = ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN, - isUserAction = true + isUserAction = true, ) runCurrent() @@ -350,7 +385,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = true, location = ExpandableViewState.LOCATION_GONE, - isUserAction = false + isUserAction = false, ) runCurrent() @@ -366,7 +401,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { /* key = */ "key", /* userAction = */ false, /* expanded = */ true, - NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, ) } @@ -378,7 +413,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = true, location = ExpandableViewState.LOCATION_GONE, - isUserAction = false + isUserAction = false, ) runCurrent() // AND we open the shade, so we log its events @@ -404,7 +439,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { /* key = */ "key", /* userAction = */ false, /* expanded = */ true, - NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, ) } @@ -416,7 +451,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = false, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = false + isUserAction = false, ) runCurrent() @@ -433,7 +468,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = true, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() @@ -443,7 +478,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { /* key = */ "key", /* userAction = */ true, /* expanded = */ true, - NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, ) // AND the Notification is expanded again @@ -451,7 +486,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key", isExpanded = false, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() @@ -461,7 +496,7 @@ class NotificationStatsLoggerTest : SysuiTestCase() { /* key = */ "key", /* userAction = */ true, /* expanded = */ false, - NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal, ) } @@ -473,14 +508,14 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key1", isExpanded = true, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() underTest.onNotificationExpansionChanged( key = "key2", isExpanded = true, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() clearInvocations(mockStatusBarService) @@ -500,14 +535,14 @@ class NotificationStatsLoggerTest : SysuiTestCase() { key = "key1", isExpanded = true, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() underTest.onNotificationExpansionChanged( key = "key2", isExpanded = true, location = ExpandableViewState.LOCATION_MAIN_AREA, - isUserAction = true + isUserAction = true, ) runCurrent() clearInvocations(mockStatusBarService) @@ -535,10 +570,15 @@ class NotificationStatsLoggerTest : SysuiTestCase() { private class NotificationVisibilitySubject(private val visibility: NotificationVisibility) { fun isKeyEqualTo(key: String) = assertThat(visibility.key).isEqualTo(key) + fun isRankEqualTo(rank: Int) = assertThat(visibility.rank).isEqualTo(rank) + fun isCountEqualTo(count: Int) = assertThat(visibility.count).isEqualTo(count) + fun isVisible() = assertThat(this.visibility.visible).isTrue() + fun notVisible() = assertThat(this.visibility.visible).isFalse() + fun isInMainArea() = assertThat(this.visibility.location) .isEqualTo(NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index f318c74e0584..7bc2bca3df02 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -43,6 +43,8 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiTestCase; import com.android.systemui.assist.AssistManager; import com.android.systemui.emergency.EmergencyGestureModule.EmergencyGestureIntentFactory; +import com.android.systemui.flags.DisableSceneContainer; +import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.plugins.ActivityStarter; @@ -55,10 +57,8 @@ import com.android.systemui.shade.CameraLauncher; import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeHeaderController; -import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; import com.android.systemui.shade.domain.interactor.ShadeInteractor; -import com.android.systemui.shade.shared.flag.DualShade; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; @@ -67,6 +67,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.wallet.controller.QuickAccessWalletController; +import dagger.Lazy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -76,8 +78,6 @@ import org.mockito.stubbing.Answer; import java.util.Optional; -import dagger.Lazy; - @SmallTest @RunWith(AndroidJUnit4.class) public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { @@ -87,7 +87,6 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { @Mock private ShadeController mShadeController; @Mock private CommandQueue mCommandQueue; @Mock private QuickSettingsController mQuickSettingsController; - @Mock private ShadeViewController mShadeViewController; @Mock private PanelExpansionInteractor mPanelExpansionInteractor; @Mock private Lazy<ShadeInteractor> mShadeInteractorLazy; @Mock private ShadeHeaderController mShadeHeaderController; @@ -242,7 +241,8 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { } @Test - @DisableFlags(value = {QSComposeFragment.FLAG_NAME, DualShade.FLAG_NAME}) + @DisableSceneContainer + @DisableFlags(QSComposeFragment.FLAG_NAME) public void clickQsTile_flagsDisabled_callsQSPanelController() { ComponentName c = new ComponentName("testpkg", "testcls"); @@ -251,7 +251,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { } @Test - @DisableFlags(DualShade.FLAG_NAME) + @DisableSceneContainer @EnableFlags(QSComposeFragment.FLAG_NAME) public void clickQsTile_onlyQSComposeFlag_callsQSHost() { ComponentName c = new ComponentName("testpkg", "testcls"); @@ -262,9 +262,9 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { } @Test - @EnableFlags(DualShade.FLAG_NAME) + @EnableSceneContainer @DisableFlags(QSComposeFragment.FLAG_NAME) - public void clickQsTile_onlyDualShadeFlag_callsQSHost() { + public void clickQsTile_onlySceneContainerFlag_callsQSHost() { ComponentName c = new ComponentName("testpkg", "testcls"); mSbcqCallbacks.clickTile(c); @@ -273,8 +273,9 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { } @Test - @EnableFlags(value = {QSComposeFragment.FLAG_NAME, DualShade.FLAG_NAME}) - public void clickQsTile_qsComposeAndDualShadeFlags_callsQSHost() { + @EnableSceneContainer + @EnableFlags(QSComposeFragment.FLAG_NAME) + public void clickQsTile_qsComposeAndSceneContainerFlags_callsQSHost() { ComponentName c = new ComponentName("testpkg", "testcls"); mSbcqCallbacks.clickTile(c); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index 1c40cd06c119..5d51c6d16c5a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -263,7 +263,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, userRestrictionChecker = mock(), - processWrapper = ProcessWrapperFake() + processWrapper = ProcessWrapperFake(activityManager) ) ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index e1d1057ea249..8ff088f5d29b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -175,7 +175,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, userRestrictionChecker = mock(), - processWrapper = ProcessWrapperFake() + processWrapper = ProcessWrapperFake(activityManager) ), guestUserInteractor = guestUserInteractor, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt deleted file mode 100644 index d0cc56860ce8..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume.panel.component.mediaoutput.domain - -import android.content.mockedContext -import android.content.packageManager -import android.content.pm.PackageManager.FEATURE_PC -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.collectLastValue -import com.android.systemui.kosmos.runTest -import com.android.systemui.kosmos.testScope -import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -class MediaOutputAvailabilityCriteriaTest : SysuiTestCase() { - - private val kosmos = testKosmos() - private val scope = kosmos.testScope - - private lateinit var underTest: MediaOutputAvailabilityCriteria - - @Before - fun setup() { - with(kosmos) { - underTest = MediaOutputAvailabilityCriteria(kosmos.mockedContext, scope.backgroundScope) - } - } - - @Test - fun isDesktop_unavailable() = - kosmos.runTest { - whenever(mockedContext.getPackageManager()).thenReturn(packageManager) - whenever(packageManager.hasSystemFeature(FEATURE_PC)).thenReturn(true) - - val isAvailable by collectLastValue(underTest.isAvailable()) - - assertThat(isAvailable).isFalse() - } - - @Test - fun notIsDesktop_available() = - kosmos.runTest { - whenever(mockedContext.getPackageManager()).thenReturn(packageManager) - whenever(packageManager.hasSystemFeature(FEATURE_PC)).thenReturn(false) - - val isAvailable by collectLastValue(underTest.isAvailable()) - - assertThat(isAvailable).isTrue() - } -} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt index 1bc9367ce3c5..6e4dc1485c7b 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt @@ -62,12 +62,11 @@ data class ClockFontAxis( fun toSetting() = ClockFontAxisSetting(key, currentValue) companion object { - fun merge( - fontAxes: List<ClockFontAxis>, - axisSettings: List<ClockFontAxisSetting>, + fun List<ClockFontAxis>.merge( + axisSettings: List<ClockFontAxisSetting> ): List<ClockFontAxis> { val result = mutableListOf<ClockFontAxis>() - for (axis in fontAxes) { + for (axis in this) { val setting = axisSettings.firstOrNull { axis.key == it.key } val output = setting?.let { axis.copy(currentValue = it.value) } ?: axis result.add(output) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt index 6128c00f3843..e7b36626a810 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt @@ -98,13 +98,20 @@ data class ClockFontAxisSetting( return result } - fun toFVar(settings: List<ClockFontAxisSetting>): String { + fun List<ClockFontAxisSetting>.toFVar(): String { val sb = StringBuilder() - for (axis in settings) { + for (axis in this) { if (sb.length > 0) sb.append(", ") sb.append("'${axis.key}' ${axis.value.toInt()}") } return sb.toString() } + + fun List<ClockFontAxisSetting>.replace( + replacements: List<ClockFontAxisSetting> + ): List<ClockFontAxisSetting> { + var remaining = this.filterNot { lhs -> replacements.any { rhs -> lhs.key == rhs.key } } + return remaining + replacements + } } } diff --git a/packages/SystemUI/res/layout/media_output_list_group_divider.xml b/packages/SystemUI/res/layout/media_output_list_group_divider.xml index 5e96866c0a9a..c351912de295 100644 --- a/packages/SystemUI/res/layout/media_output_list_group_divider.xml +++ b/packages/SystemUI/res/layout/media_output_list_group_divider.xml @@ -26,7 +26,7 @@ android:layout_width="wrap_content" android:layout_height="36dp" android:layout_gravity="center_vertical|start" - android:layout_marginStart="16dp" + android:layout_marginStart="@dimen/media_output_dialog_margin_horizontal" android:layout_marginEnd="56dp" android:ellipsize="end" android:maxLines="1" diff --git a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml index 69117cf7cf5d..d297ec46e1e1 100644 --- a/packages/SystemUI/res/layout/media_output_list_item_advanced.xml +++ b/packages/SystemUI/res/layout/media_output_list_item_advanced.xml @@ -15,18 +15,19 @@ ~ limitations under the License. --> -<FrameLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/device_container" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:paddingHorizontal="@dimen/media_output_dialog_margin_horizontal" + android:baselineAligned="false"> <FrameLayout - android:layout_width="match_parent" - android:layout_height="64dp" + android:layout_weight="1" + android:layout_width="0dp" + android:layout_height="@dimen/media_output_dialog_item_height" android:id="@+id/item_layout" android:background="@drawable/media_output_item_background" - android:layout_marginStart="16dp" - android:layout_marginEnd="80dp" android:layout_marginBottom="12dp"> <FrameLayout android:layout_width="match_parent" @@ -36,7 +37,7 @@ android:id="@+id/volume_seekbar" android:splitTrack="false" android:visibility="gone" - android:paddingStart="64dp" + android:paddingStart="@dimen/media_output_dialog_item_height" android:paddingEnd="0dp" android:background="@null" android:contentDescription="@string/media_output_dialog_accessibility_seekbar" @@ -48,8 +49,8 @@ <FrameLayout android:id="@+id/icon_area" - android:layout_width="64dp" - android:layout_height="64dp" + android:layout_width="@dimen/media_output_dialog_item_height" + android:layout_height="@dimen/media_output_dialog_item_height" android:focusable="false" android:importantForAccessibility="no" android:layout_gravity="center_vertical|start"> @@ -131,11 +132,11 @@ </FrameLayout> <FrameLayout android:id="@+id/end_action_area" - android:layout_width="64dp" - android:layout_height="64dp" + android:layout_width="@dimen/media_output_dialog_item_height" + android:layout_height="@dimen/media_output_dialog_item_height" android:visibility="gone" android:layout_marginBottom="6dp" - android:layout_marginEnd="8dp" + android:layout_marginStart="7dp" android:layout_gravity="end|center" android:gravity="center" android:background="@drawable/media_output_item_background_active"> @@ -160,4 +161,4 @@ android:indeterminateOnly="true" android:visibility="gone"/> </FrameLayout> -</FrameLayout>
\ No newline at end of file +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml index 7f7350472fa5..cb7bd1728077 100644 --- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml +++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml @@ -23,6 +23,7 @@ android:orientation="vertical" android:padding="@dimen/magnification_setting_background_padding" android:focusable="true" + android:accessibilityPaneTitle="@string/accessibility_magnification_settings_panel_description" android:contentDescription="@string/accessibility_magnification_settings_panel_description"> <LinearLayout android:layout_width="match_parent" diff --git a/packages/SystemUI/res/values-ldrtl/dimens.xml b/packages/SystemUI/res/values-ldrtl/dimens.xml index 0d99b617819b..345f0414e637 100644 --- a/packages/SystemUI/res/values-ldrtl/dimens.xml +++ b/packages/SystemUI/res/values-ldrtl/dimens.xml @@ -16,5 +16,5 @@ --> <resources> <dimen name="media_output_dialog_icon_left_radius">0dp</dimen> - <dimen name="media_output_dialog_icon_right_radius">28dp</dimen> + <dimen name="media_output_dialog_icon_right_radius">@dimen/media_output_dialog_active_background_radius</dimen> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 26f32ef60851..d22c8d910230 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -126,4 +126,6 @@ <dimen name="controls_content_padding">24dp</dimen> <dimen name="control_list_vertical_spacing">8dp</dimen> <dimen name="control_list_horizontal_spacing">16dp</dimen> + + <dimen name="communal_to_dream_button_size">64dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index a96ebe7b4fd6..d93716b03685 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -953,6 +953,8 @@ <dimen name="communal_widget_picker_desired_width">360dp</dimen> <dimen name="communal_widget_picker_desired_height">240dp</dimen> + <dimen name="communal_to_dream_button_size">48dp</dimen> + <!-- The width/height of the unlock icon view on keyguard. --> <dimen name="keyguard_lock_height">42dp</dimen> <dimen name="keyguard_lock_padding">20dp</dimen> @@ -1523,11 +1525,11 @@ <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen> <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen> <dimen name="media_output_dialog_background_radius">16dp</dimen> - <dimen name="media_output_dialog_active_background_radius">30dp</dimen> - <dimen name="media_output_dialog_default_margin_end">16dp</dimen> - <dimen name="media_output_dialog_selectable_margin_end">80dp</dimen> + <dimen name="media_output_dialog_active_background_radius">32dp</dimen> + <dimen name="media_output_dialog_item_height">64dp</dimen> + <dimen name="media_output_dialog_margin_horizontal">16dp</dimen> <dimen name="media_output_dialog_list_padding_top">8dp</dimen> - <dimen name="media_output_dialog_icon_left_radius">28dp</dimen> + <dimen name="media_output_dialog_icon_left_radius">@dimen/media_output_dialog_active_background_radius</dimen> <dimen name="media_output_dialog_icon_right_radius">0dp</dimen> <!-- Distance that the full shade transition takes in order to complete by tapping on a button diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index 9d9f5691816e..c14d28d1c08d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -392,15 +392,6 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest setSystemGestureExclusion(); mIsVisible = true; mCallback.onSettingsPanelVisibilityChanged(/* shown= */ true); - - if (resetPosition) { - // We could not put focus on the settings panel automatically - // since it is an inactive window. Therefore, we announce the existence of - // magnification settings for accessibility when it is opened. - mSettingView.announceForAccessibility( - mContext.getResources().getString( - R.string.accessibility_magnification_settings_panel_description)); - } } mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index fd50485fc3a3..372fdca20ed9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -3728,12 +3728,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // windows that appear on top, ever int flags = StatusBarManager.DISABLE_NONE; - // TODO (b/155663717) After restart, status bar will not properly hide home button + // TODO(b/155663717): After restart, status bar will not properly hide home button // unless disable is called to show un-hide it once first if (forceClearFlags) { if (UserManager.isVisibleBackgroundUsersEnabled() - && !mProcessWrapper.isSystemUser() && !mProcessWrapper.isForegroundUser()) { - // TODO: b/341604160 - Support visible background users properly. + && !mProcessWrapper.isSystemUser() + && !mProcessWrapper.isForegroundUserOrProfile()) { + // TODO(b/341604160): Support visible background users properly. if (DEBUG) { Log.d(TAG, "Status bar manager is disabled for visible background users"); } @@ -3769,8 +3770,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (!SceneContainerFlag.isEnabled()) { if (UserManager.isVisibleBackgroundUsersEnabled() - && !mProcessWrapper.isSystemUser() && !mProcessWrapper.isForegroundUser()) { - // TODO: b/341604160 - Support visible background users properly. + && !mProcessWrapper.isSystemUser() + && !mProcessWrapper.isForegroundUserOrProfile()) { + // TODO(b/341604160): Support visible background users properly. if (DEBUG) { Log.d(TAG, "Status bar manager is disabled for visible background users"); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index aaad10140a92..4b36e7a43dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -79,6 +79,8 @@ interface KeyguardRepository { val panelAlpha: MutableStateFlow<Float> + val zoomOut: StateFlow<Float> + /** * Observable for whether the keyguard is showing. * @@ -278,6 +280,9 @@ interface KeyguardRepository { /** Temporary shim for fading out content when the brightness slider is used */ fun setPanelAlpha(alpha: Float) + /** Sets the zoom out scale of spatial model pushback from e.g. pulling down the shade. */ + fun setZoomOut(zoomOutFromShadeRadius: Float) + /** Whether the device is actively dreaming */ fun setDreaming(isDreaming: Boolean) @@ -381,6 +386,7 @@ constructor( override val onCameraLaunchDetected = MutableStateFlow(CameraLaunchSourceModel()) override val panelAlpha: MutableStateFlow<Float> = MutableStateFlow(1f) + override val zoomOut: MutableStateFlow<Float> = MutableStateFlow(0f) override val topClippingBounds = MutableStateFlow<Int?>(null) override val isKeyguardShowing: MutableStateFlow<Boolean> = @@ -662,6 +668,10 @@ constructor( panelAlpha.value = alpha } + override fun setZoomOut(zoomOutFromShadeRadius: Float) { + zoomOut.value = zoomOutFromShadeRadius + } + override fun setDreaming(isDreaming: Boolean) { this.isDreaming.value = isDreaming } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 3739d17da6c4..3652c17309f4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -337,6 +337,9 @@ constructor( @Deprecated("SceneContainer uses NotificationStackAppearanceInteractor") val panelAlpha: StateFlow<Float> = repository.panelAlpha.asStateFlow() + /** Sets the zoom out scale of spatial model pushback from e.g. pulling down the shade. */ + val zoomOut: StateFlow<Float> = repository.zoomOut + /** * When the lockscreen can be dismissed, emit an alpha value as the user swipes up. This is * useful just before the code commits to moving to GONE. @@ -475,6 +478,10 @@ constructor( repository.setPanelAlpha(alpha) } + fun setZoomOut(zoomOutFromShadeRadius: Float) { + repository.setZoomOut(zoomOutFromShadeRadius) + } + fun setAnimateDozingTransitions(animate: Boolean) { repository.setAnimateDozingTransitions(animate) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt index d6a110a8fd55..cb602f1287f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt @@ -22,7 +22,10 @@ import android.content.Context import android.os.Binder import android.os.IBinder import android.os.RemoteException +import android.os.UserManager import android.provider.DeviceConfig +import android.util.Log +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.config.sysui.SystemUiDeviceConfigFlags import com.android.internal.statusbar.IStatusBarService import com.android.systemui.CoreStartable @@ -39,6 +42,7 @@ import com.android.systemui.navigation.domain.interactor.NavigationInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessModel +import com.android.systemui.process.ProcessWrapper import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.user.domain.interactor.SelectedUserInteractor @@ -49,9 +53,10 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext +private val TAG = StatusBarDisableFlagsInteractor::class.simpleName + /** * Logic around StatusBarService#disableForUser, which is used to disable the home and recents * button in certain device states. @@ -67,6 +72,7 @@ constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, private val statusBarService: IStatusBarService, + private val processWrapper: ProcessWrapper, keyguardTransitionInteractor: KeyguardTransitionInteractor, selectedUserInteractor: SelectedUserInteractor, deviceConfigInteractor: DeviceConfigInteractor, @@ -141,6 +147,24 @@ constructor( return } + // TODO(b/341604160): Remove this blocking logic once StatusBarManagerService supports + // visible background users properly. + if ( + UserManager.isVisibleBackgroundUsersEnabled() && + !processWrapper.isSystemUser() && + !processWrapper.isForegroundUserOrProfile() + ) { + // Currently, only one SysUI process can register with IStatusBarService to listen + // for the CommandQueue events. + // In the Multi Display configuration with concurrent multi users (primarily used + // in Automotive), a visible background user (Automotive Multi Display passengers) + // could also access this code path. Given this limitation and we only allow the + // current user's SysUI process to register with IStatusBarService, we need to prevent + // calls into IStatusBarService from visible background users. + Log.d(TAG, "Status bar manager is disabled for visible background users") + return + } + scope.launch { disableFlagsForUserId.collect { (selectedUserId, flags) -> if (context.getSystemService(Context.STATUS_BAR_SERVICE) == null) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index e48af773497a..b8020b19ce86 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -172,6 +172,13 @@ object KeyguardRootViewBinder { } } + launch("$TAG#zoomOut") { + viewModel.scaleFromZoomOut.collect { scaleFromZoomOut -> + view.scaleX = scaleFromZoomOut + view.scaleY = scaleFromZoomOut + } + } + launch("$TAG#translationY") { // When translation happens in burnInLayer, it won't be weather clock large // clock isn't added to burnInLayer due to its scale transition so we also diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 11a509a4fa61..47a76a00fd4a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -287,6 +287,9 @@ constructor( .distinctUntilChanged() } + val scaleFromZoomOut: Flow<Float> = + keyguardInteractor.zoomOut.map { 1 - it * PUSHBACK_SCALE_FOR_LOCKSCREEN } + val translationY: Flow<Float> = aodBurnInViewModel.movement.map { it.translationY.toFloat() } val translationX: Flow<StateToValue> = @@ -418,5 +421,6 @@ constructor( companion object { private const val TAG = "KeyguardRootViewModel" + private const val PUSHBACK_SCALE_FOR_LOCKSCREEN = 0.05f } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index ef6ae0dd6427..b6a3b6aaba14 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -24,10 +24,11 @@ import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.ClockSize +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.res.R -import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import dagger.assisted.AssistedFactory @@ -54,8 +55,8 @@ constructor( val touchHandling: KeyguardTouchHandlingViewModel, private val shadeInteractor: ShadeInteractor, private val unfoldTransitionInteractor: UnfoldTransitionInteractor, - private val occlusionInteractor: SceneContainerOcclusionInteractor, private val deviceEntryInteractor: DeviceEntryInteractor, + private val transitionInteractor: KeyguardTransitionInteractor, ) : ExclusiveActivatable() { @VisibleForTesting val clockSize = clockInteractor.clockSize @@ -89,9 +90,15 @@ constructor( } launch { - occlusionInteractor.isOccludingActivityShown - .map { !it } - .collect { _isContentVisible.value = it } + transitionInteractor + .transitionValue(KeyguardState.OCCLUDED) + .map { it > 0f } + .collect { fullyOrPartiallyOccluded -> + // Content is visible unless we're OCCLUDED. Currently, we don't have nice + // animations into and out of OCCLUDED, so the lockscreen/AOD content is + // hidden immediately upon entering/exiting OCCLUDED. + _isContentVisible.value = !fullyOrPartiallyOccluded + } } awaitCancellation() diff --git a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java index e8ded03e3b38..ad306694346e 100644 --- a/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java +++ b/packages/SystemUI/src/com/android/systemui/log/SessionTracker.java @@ -116,8 +116,8 @@ public class SessionTracker implements CoreStartable { mSessionToInstanceId.put(type, instanceId); if (UserManager.isVisibleBackgroundUsersEnabled() && !mProcessWrapper.isSystemUser() - && !mProcessWrapper.isForegroundUser()) { - // TODO: b/341604160 - Support visible background users properly. + && !mProcessWrapper.isForegroundUserOrProfile()) { + // TODO(b/341604160): Support visible background users properly. if (DEBUG) { Log.d(TAG, "Status bar manager is disabled for visible background users"); } @@ -155,8 +155,8 @@ public class SessionTracker implements CoreStartable { mUiEventLogger.log(endSessionUiEvent, instanceId); } if (UserManager.isVisibleBackgroundUsersEnabled() && !mProcessWrapper.isSystemUser() - && !mProcessWrapper.isForegroundUser()) { - // TODO: b/341604160 - Support visible background users properly. + && !mProcessWrapper.isForegroundUserOrProfile()) { + // TODO(b/341604160): Support visible background users properly. if (DEBUG) { Log.d(TAG, "Status bar manager is disabled for visible background users"); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index b391cb079ec5..bc6b2beb2ddb 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -298,8 +298,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { boolean showEndArea = !Flags.enableOutputSwitcherSessionGrouping() || isDeselectable; updateUnmutedVolumeIcon(device); - updateGroupableCheckBox(true, isDeselectable, device); - updateEndClickArea(device, isDeselectable); + updateEndAreaForGroupCheckbox(device, true /* isSelected */, isDeselectable); disableFocusPropertyForView(mContainerLayout); setUpContentDescriptionForView(mSeekBar, device); setSingleLineLayout(device.getName(), true /* showSeekBar */, @@ -331,8 +330,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { //If device is connected and there's other selectable devices, layout as // one of selected devices. updateUnmutedVolumeIcon(device); - updateGroupableCheckBox(true, isDeselectable, device); - updateEndClickArea(device, isDeselectable); + updateEndAreaForGroupCheckbox(device, true /* isSelected */, + isDeselectable); disableFocusPropertyForView(mContainerLayout); setUpContentDescriptionForView(mSeekBar, device); setSingleLineLayout(device.getName(), true /* showSeekBar */, @@ -352,8 +351,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } else if (isSelectable) { //groupable device setUpDeviceIcon(device); - updateGroupableCheckBox(false, true, device); - updateEndClickArea(device, true); + updateEndAreaForGroupCheckbox(device, false /* isSelected */, + true /* isDeselectable */); if (!Flags.disableTransferWhenAppsDoNotSupport() || isTransferable || hasRouteListingPreferenceItem) { @@ -406,7 +405,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { private void updateEndClickAreaWithIcon(View.OnClickListener clickListener, @DrawableRes int iconDrawableId, @StringRes int accessibilityStringId) { - updateEndClickAreaColor(mController.getColorSeekbarProgress()); + updateEndAreaColor(mController.getColorSeekbarProgress()); mEndClickIcon.setImageTintList( ColorStateList.valueOf(mController.getColorItemContent())); mEndClickIcon.setOnClickListener(clickListener); @@ -422,7 +421,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } } - public void updateEndClickAreaColor(int color) { + public void updateEndAreaColor(int color) { mEndTouchArea.setBackgroundTintList( ColorStateList.valueOf(color)); } @@ -455,25 +454,22 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { ColorStateList.valueOf(mController.getColorItemContent())); } - public void updateEndClickArea(MediaDevice device, boolean isDeviceDeselectable) { + public void updateEndAreaForGroupCheckbox(MediaDevice device, boolean isSelected, + boolean isDeselectable) { mEndTouchArea.setOnClickListener(null); mEndTouchArea.setOnClickListener( - isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null); + isDeselectable ? (v) -> mCheckBox.performClick() : null); mEndTouchArea.setImportantForAccessibility( View.IMPORTANT_FOR_ACCESSIBILITY_YES); - mEndTouchArea.setBackgroundTintList( - ColorStateList.valueOf(mController.getColorItemBackground())); + updateEndAreaColor(isSelected ? mController.getColorSeekbarProgress() + : mController.getColorItemBackground()); setUpContentDescriptionForView(mEndTouchArea, device); - } - - private void updateGroupableCheckBox(boolean isSelected, boolean isGroupable, - MediaDevice device) { mCheckBox.setOnCheckedChangeListener(null); mCheckBox.setChecked(isSelected); mCheckBox.setOnCheckedChangeListener( - isGroupable ? (buttonView, isChecked) -> onGroupActionTriggered(!isSelected, + isDeselectable ? (buttonView, isChecked) -> onGroupActionTriggered(!isSelected, device) : null); - mCheckBox.setEnabled(isGroupable); + mCheckBox.setEnabled(isDeselectable); setCheckBoxColor(mCheckBox, mController.getColorItemContent()); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index ee2d8aa46264..a7786c8f0b57 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -141,6 +141,8 @@ public abstract class MediaOutputBaseAdapter extends final ImageView mEndClickIcon; @VisibleForTesting MediaOutputSeekbar mSeekBar; + private final float mInactiveRadius; + private final float mActiveRadius; private String mDeviceId; private ValueAnimator mCornerAnimator; private ValueAnimator mVolumeAnimator; @@ -161,6 +163,10 @@ public abstract class MediaOutputBaseAdapter extends mEndClickIcon = view.requireViewById(R.id.media_output_item_end_click_icon); mVolumeValueText = view.requireViewById(R.id.volume_value); mIconAreaLayout = view.requireViewById(R.id.icon_area); + mInactiveRadius = mContext.getResources().getDimension( + R.dimen.media_output_dialog_background_radius); + mActiveRadius = mContext.getResources().getDimension( + R.dimen.media_output_dialog_active_background_radius); initAnimator(); } @@ -216,10 +222,6 @@ public abstract class MediaOutputBaseAdapter extends mEndClickIcon.setVisibility( !showCheckBox && showEndTouchArea ? View.VISIBLE : View.GONE); } - ViewGroup.MarginLayoutParams params = - (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams(); - params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable() - : mController.getItemMarginEndDefault(); } void setTwoLineLayout(CharSequence title, boolean showSeekBar, @@ -247,10 +249,6 @@ public abstract class MediaOutputBaseAdapter extends //update end click area by isActive mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); mEndClickIcon.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); - ViewGroup.MarginLayoutParams params = - (ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams(); - params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable() - : mController.getItemMarginEndDefault(); mItemLayout.setBackground(backgroundDrawable); mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE); mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE); @@ -264,10 +262,10 @@ public abstract class MediaOutputBaseAdapter extends final GradientDrawable progressDrawable = (GradientDrawable) clipDrawable.getDrawable(); progressDrawable.setCornerRadii( - new float[]{0, 0, mController.getActiveRadius(), - mController.getActiveRadius(), - mController.getActiveRadius(), - mController.getActiveRadius(), 0, 0}); + new float[]{0, 0, mActiveRadius, + mActiveRadius, + mActiveRadius, + mActiveRadius, 0, 0}); } private void initializeSeekbarVolume( @@ -431,8 +429,7 @@ public abstract class MediaOutputBaseAdapter extends } private void initAnimator() { - mCornerAnimator = ValueAnimator.ofFloat(mController.getInactiveRadius(), - mController.getActiveRadius()); + mCornerAnimator = ValueAnimator.ofFloat(mInactiveRadius, mActiveRadius); mCornerAnimator.setDuration(ANIM_DURATION); mCornerAnimator.setInterpolator(new LinearInterpolator()); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java index 02a2befe44e5..19409b32a2f6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java @@ -171,10 +171,6 @@ public class MediaSwitchingController private int mColorConnectedItemBackground; private int mColorPositiveButtonText; private int mColorDialogBackground; - private int mItemMarginEndDefault; - private int mItemMarginEndSelectable; - private float mInactiveRadius; - private float mActiveRadius; private FeatureFlags mFeatureFlags; private UserTracker mUserTracker; private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor; @@ -246,16 +242,8 @@ public class MediaSwitchingController R.color.media_dialog_connected_item_background); mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_solid_button_text); - mInactiveRadius = mContext.getResources().getDimension( - R.dimen.media_output_dialog_background_radius); - mActiveRadius = mContext.getResources().getDimension( - R.dimen.media_output_dialog_active_background_radius); mColorDialogBackground = Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_background); - mItemMarginEndDefault = (int) mContext.getResources().getDimension( - R.dimen.media_output_dialog_default_margin_end); - mItemMarginEndSelectable = (int) mContext.getResources().getDimension( - R.dimen.media_output_dialog_selectable_margin_end); if (enableInputRouting()) { mInputRouteManager = new InputRouteManager(mContext, audioManager); @@ -638,22 +626,6 @@ public class MediaSwitchingController return mColorItemBackground; } - public float getInactiveRadius() { - return mInactiveRadius; - } - - public float getActiveRadius() { - return mActiveRadius; - } - - public int getItemMarginEndDefault() { - return mItemMarginEndDefault; - } - - public int getItemMarginEndSelectable() { - return mItemMarginEndSelectable; - } - private void buildMediaItems(List<MediaDevice> devices) { synchronized (mMediaDevicesLock) { List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices); diff --git a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java index 294d0c75167a..f3a3a3a2ac4c 100644 --- a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java @@ -27,8 +27,12 @@ import javax.inject.Inject; * providing a mockable target around these details. */ public class ProcessWrapper { + private final ActivityManager mActivityManager; + @Inject - public ProcessWrapper() {} + public ProcessWrapper(ActivityManager activityManager) { + mActivityManager = activityManager; + } /** * Returns {@code true} if System User is running the current process. @@ -38,10 +42,10 @@ public class ProcessWrapper { } /** - * Returns {@code true} if the foreground user is running the current process. + * Returns {@code true} if the foreground user or profile is running the current process. */ - public boolean isForegroundUser() { - return ActivityManager.getCurrentUser() == myUserHandle().getIdentifier(); + public boolean isForegroundUserOrProfile() { + return mActivityManager.isProfileForeground(myUserHandle()); } /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt index c302cb21f77f..3afaef5ea6a1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QsDetailedView.kt @@ -20,7 +20,6 @@ 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") @@ -37,7 +36,6 @@ object QsDetailedView { inline val isEnabled get() = Flags.qsTileDetailedView() && // mainAconfigFlag - DualShade.isEnabled && SceneContainerFlag.isEnabled // NOTE: Changes should also be made in getSecondaryFlags @@ -47,10 +45,8 @@ object QsDetailedView { /** 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() + // NOTE: Changes should also be made in isEnabled + SceneContainerFlag.getAllRequirements() /** The full set of requirements for QsDetailedView */ inline fun getAllRequirements(): Sequence<FlagToken> { diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt index 3067ccbb7cea..3140df8d947a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt @@ -17,11 +17,11 @@ package com.android.systemui.qs.flags import com.android.systemui.flags.RefactorFlagUtils -import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.scene.shared.flag.SceneContainerFlag /** * Object to help check if the new QS ui should be used. This is true if either [QSComposeFragment] - * or [DualShade] are enabled. + * or [SceneContainerFlag] are enabled. */ object QsInCompose { @@ -29,11 +29,12 @@ object QsInCompose { * This is not a real flag name, but a representation of the allowed flag names. Should not be * used with test annotations. */ - private val flagName = "${QSComposeFragment.FLAG_NAME}|${DualShade.FLAG_NAME}" + private val flagName = + "${QSComposeFragment.FLAG_NAME}|${SceneContainerFlag.getMainAconfigFlag().name}" @JvmStatic inline val isEnabled: Boolean - get() = QSComposeFragment.isEnabled || DualShade.isEnabled + get() = QSComposeFragment.isEnabled || SceneContainerFlag.isEnabled @JvmStatic fun isUnexpectedlyInLegacyMode() = diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 28f5694c3332..e535019cd3d7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -527,6 +527,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final ActivityStarter mActivityStarter; private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor; + @Nullable + private RenderEffect mBlurRenderEffect = null; + @Inject public NotificationPanelViewController(NotificationPanelView view, NotificationWakeUpCoordinator coordinator, @@ -912,13 +915,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void handleBouncerShowingChanged(Boolean isBouncerShowing) { if (!com.android.systemui.Flags.bouncerUiRevamp()) return; - if (isBouncerShowing && isExpanded()) { - float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx(); - mView.setRenderEffect(RenderEffect.createBlurEffect( - shadeBlurEffect, - shadeBlurEffect, - Shader.TileMode.CLAMP)); + if (mBlurRenderEffect == null) { + mBlurRenderEffect = RenderEffect.createBlurEffect( + mDepthController.getMaxBlurRadiusPx(), + mDepthController.getMaxBlurRadiusPx(), + Shader.TileMode.CLAMP); + } + mView.setRenderEffect(mBlurRenderEffect); } else { mView.setRenderEffect(null); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index a2a840942f3c..e3b36df9aed7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -40,6 +40,7 @@ import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionListener @@ -74,6 +75,7 @@ constructor( private val blurUtils: BlurUtils, private val biometricUnlockController: BiometricUnlockController, private val keyguardStateController: KeyguardStateController, + private val keyguardInteractor: KeyguardInteractor, private val choreographer: Choreographer, private val wallpaperController: WallpaperController, private val notificationShadeWindowController: NotificationShadeWindowController, @@ -281,6 +283,7 @@ constructor( appZoomOutOptional.ifPresent { appZoomOut -> appZoomOut.setProgress(zoomOutFromShadeRadius) } + keyguardInteractor.setZoomOut(zoomOutFromShadeRadius) } listeners.forEach { it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 5cc79df9130a..09cc3f23032e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -812,11 +812,6 @@ public final class NotificationEntry extends ListEntry { return !mSbn.isOngoing() || !isLocked; } - public boolean canViewBeDismissed() { - if (row == null) return true; - return row.canViewBeDismissed(); - } - @VisibleForTesting boolean isExemptFromDndVisualSuppression() { if (isNotificationBlockedByPolicy(mSbn.getNotification())) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt deleted file mode 100644 index 56057fb00e45..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.promoted.ui.viewmodel - -import android.graphics.drawable.Icon -import com.android.internal.widget.NotificationProgressModel -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style -import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class PromotedNotificationViewModel( - identity: PromotedNotificationContentModel.Identity, - content: Flow<PromotedNotificationContentModel>, -) { - // for all styles: - - val key: String = identity.key - val style: Style = identity.style - - val skeletonSmallIcon: Flow<Icon?> = content.map { it.skeletonSmallIcon } - val appName: Flow<CharSequence?> = content.map { it.appName } - val subText: Flow<CharSequence?> = content.map { it.subText } - - private val time: Flow<When?> = content.map { it.time } - val whenTime: Flow<Long?> = time.map { it?.time } - val whenMode: Flow<When.Mode?> = time.map { it?.mode } - - val lastAudiblyAlertedMs: Flow<Long> = content.map { it.lastAudiblyAlertedMs } - val profileBadgeResId: Flow<Int?> = content.map { it.profileBadgeResId } - val title: Flow<CharSequence?> = content.map { it.title } - val text: Flow<CharSequence?> = content.map { it.text } - val skeletonLargeIcon: Flow<Icon?> = content.map { it.skeletonLargeIcon } - - // for CallStyle: - val personIcon: Flow<Icon?> = content.map { it.personIcon } - val personName: Flow<CharSequence?> = content.map { it.personName } - val verificationIcon: Flow<Icon?> = content.map { it.verificationIcon } - val verificationText: Flow<CharSequence?> = content.map { it.verificationText } - - // for ProgressStyle: - val progress: Flow<NotificationProgressModel?> = content.map { it.newProgress } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index aa7665a5b630..15f316800000 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -71,7 +71,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.dynamicanimation.animation.FloatPropertyCompat; import androidx.dynamicanimation.animation.SpringAnimation; -import androidx.dynamicanimation.animation.SpringForce; import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; @@ -361,39 +360,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } }; - private final SpringAnimation mMagneticAnimator = new SpringAnimation( - this, FloatPropertyCompat.createFloatPropertyCompat(TRANSLATE_CONTENT)); - - private final MagneticRowListener mMagneticRowListener = new MagneticRowListener() { - - @Override - public void setMagneticTranslation(float translation) { - if (mMagneticAnimator.isRunning()) { - mMagneticAnimator.animateToFinalPosition(translation); - } else { - setTranslation(translation); - } - } - - @Override - public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce, - float startVelocity) { - cancelMagneticAnimations(); - mMagneticAnimator.setSpring(springForce); - mMagneticAnimator.setStartVelocity(startVelocity); - mMagneticAnimator.animateToFinalPosition(endTranslation); - } - - @Override - public void cancelMagneticAnimations() { - cancelSnapBackAnimation(); - cancelTranslateAnimation(); - mMagneticAnimator.cancel(); - } - }; + @Override + protected void cancelTranslationAnimations() { + cancelSnapBackAnimation(); + cancelTranslateAnimation(); + } private void cancelSnapBackAnimation() { - PhysicsAnimator<ExpandableNotificationRow> animator = + PhysicsAnimator<ExpandableView> animator = PhysicsAnimator.getInstanceIfExists(this /* target */); if (animator != null) { animator.cancel(); @@ -2044,6 +2018,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView new NotificationInlineImageCache()); float radius = getResources().getDimension(R.dimen.notification_corner_radius_small); mSmallRoundness = radius / getMaxRadius(); + mMagneticAnimator = new SpringAnimation( + this, FloatPropertyCompat.createFloatPropertyCompat(TRANSLATE_CONTENT)); initDimens(); } @@ -3300,6 +3276,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * For the case of an {@link ExpandableNotificationRow}, the dismissibility of the row considers + * the exposure of guts, the state of the notification entry, and if the view itself is allowed + * to be dismissed. + */ + @Override + public boolean canExpandableViewBeDismissed() { + if (areGutsExposed() || !mEntry.hasFinishedInitialization()) { + return false; + } + return canViewBeDismissed(); + } + + /** * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as * otherwise some state might not be updated. */ @@ -4067,6 +4056,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout}; } + @VisibleForTesting + public void setMagneticRowListener(MagneticRowListener listener) { + mMagneticRowListener = listener; + } + /** * Equivalent to View.OnLongClickListener with coordinates */ @@ -4317,8 +4311,4 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } mLogger.logRemoveTransientRow(row.getEntry(), getEntry()); } - - public MagneticRowListener getMagneticRowListener() { - return mMagneticRowListener; - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index f83a1d9b7833..76ba7f9ea901 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -33,6 +33,9 @@ import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; import com.android.app.animation.Interpolators; import com.android.systemui.Dumpable; @@ -42,6 +45,7 @@ import com.android.systemui.statusbar.notification.Roundable; import com.android.systemui.statusbar.notification.RoundableState; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; +import com.android.systemui.statusbar.notification.stack.MagneticRowListener; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.util.Compile; import com.android.systemui.util.DumpUtilsKt; @@ -85,6 +89,55 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro protected boolean mLastInSection; protected boolean mFirstInSection; + protected SpringAnimation mMagneticAnimator = new SpringAnimation( + this /* object */, DynamicAnimation.TRANSLATION_X); + + protected MagneticRowListener mMagneticRowListener = new MagneticRowListener() { + + @Override + public void setMagneticTranslation(float translation) { + if (mMagneticAnimator.isRunning()) { + mMagneticAnimator.animateToFinalPosition(translation); + } else { + setTranslation(translation); + } + } + + @Override + public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce, + float startVelocity) { + cancelMagneticAnimations(); + mMagneticAnimator.setSpring(springForce); + mMagneticAnimator.setStartVelocity(startVelocity); + mMagneticAnimator.animateToFinalPosition(endTranslation); + } + + @Override + public void cancelMagneticAnimations() { + cancelTranslationAnimations(); + mMagneticAnimator.cancel(); + } + + @Override + public boolean canRowBeDismissed() { + return canExpandableViewBeDismissed(); + } + }; + + /** + * @return true if the ExpandableView can be dismissed. False otherwise. + */ + public boolean canExpandableViewBeDismissed() { + return false; + } + + /** Cancel any trailing animations on the translation of the view */ + protected void cancelTranslationAnimations(){} + + public MagneticRowListener getMagneticRowListener() { + return mMagneticRowListener; + } + public ExpandableView(Context context, AttributeSet attrs) { super(context, attrs); mViewState = createExpandableViewState(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt index 61aaf4998e87..3941700496f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt @@ -57,7 +57,7 @@ constructor( SpringForce().setStiffness(SNAP_BACK_STIFFNESS).setDampingRatio(SNAP_BACK_DAMPING_RATIO) // Multiplier applied to the translation of a row while swiped - private val swipedRowMultiplier = + val swipedRowMultiplier = MAGNETIC_TRANSLATION_MULTIPLIERS[MAGNETIC_TRANSLATION_MULTIPLIERS.size / 2] override fun setSwipeThresholdPx(thresholdPx: Float) { @@ -111,24 +111,22 @@ constructor( ): Boolean { if (!row.isSwipedTarget()) return false + val canTargetBeDismissed = + currentMagneticListeners.swipedListener()?.canRowBeDismissed() ?: false when (currentState) { State.IDLE -> { logger.logMagneticRowTranslationNotSet(currentState, row.entry) return false } State.TARGETS_SET -> { - pullTargets(translation) + pullTargets(translation, canTargetBeDismissed) currentState = State.PULLING } State.PULLING -> { - val targetTranslation = swipedRowMultiplier * translation - val crossedThreshold = abs(targetTranslation) >= magneticDetachThreshold - if (crossedThreshold) { - snapNeighborsBack() - currentMagneticListeners.swipedListener()?.let { detach(it, translation) } - currentState = State.DETACHED + if (canTargetBeDismissed) { + pullDismissibleRow(translation) } else { - pullTargets(translation) + pullTargets(translation, canSwipedBeDismissed = false) } } State.DETACHED -> { @@ -139,23 +137,49 @@ constructor( return true } - private fun pullTargets(translation: Float) { + private fun pullDismissibleRow(translation: Float) { + val targetTranslation = swipedRowMultiplier * translation + val crossedThreshold = abs(targetTranslation) >= magneticDetachThreshold + if (crossedThreshold) { + snapNeighborsBack() + currentMagneticListeners.swipedListener()?.let { detach(it, translation) } + currentState = State.DETACHED + } else { + pullTargets(translation, canSwipedBeDismissed = true) + } + } + + private fun pullTargets(translation: Float, canSwipedBeDismissed: Boolean) { var targetTranslation: Float currentMagneticListeners.forEachIndexed { i, listener -> - targetTranslation = MAGNETIC_TRANSLATION_MULTIPLIERS[i] * translation - listener?.setMagneticTranslation(targetTranslation) + listener?.let { + if (!canSwipedBeDismissed || !it.canRowBeDismissed()) { + // Use a reduced translation if the target swiped can't be dismissed or if the + // target itself can't be dismissed + targetTranslation = + MAGNETIC_TRANSLATION_MULTIPLIERS[i] * translation * MAGNETIC_REDUCTION + } else { + targetTranslation = MAGNETIC_TRANSLATION_MULTIPLIERS[i] * translation + } + it.setMagneticTranslation(targetTranslation) + } } - playPullHaptics(mappedTranslation = swipedRowMultiplier * translation) + playPullHaptics(mappedTranslation = swipedRowMultiplier * translation, canSwipedBeDismissed) } - private fun playPullHaptics(mappedTranslation: Float) { + private fun playPullHaptics(mappedTranslation: Float, canSwipedBeDismissed: Boolean) { val normalizedTranslation = abs(mappedTranslation) / magneticDetachThreshold - val vibrationScale = - (normalizedTranslation * MAX_VIBRATION_SCALE).pow(VIBRATION_PERCEPTION_EXPONENT) + val scaleFactor = + if (canSwipedBeDismissed) { + WEAK_VIBRATION_SCALE + } else { + STRONG_VIBRATION_SCALE + } + val vibrationScale = scaleFactor * normalizedTranslation msdlPlayer.playToken( MSDLToken.DRAG_INDICATOR_CONTINUOUS, InteractionProperties.DynamicVibrationScale( - scale = vibrationScale, + scale = vibrationScale.pow(VIBRATION_PERCEPTION_EXPONENT), vibrationAttributes = VIBRATION_ATTRIBUTES_PIPELINING, ), ) @@ -233,6 +257,8 @@ constructor( */ private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.18f, 0.28f, 0.5f, 0.28f, 0.18f) + const val MAGNETIC_REDUCTION = 0.65f + /** Spring parameters for physics animators */ private const val DETACH_STIFFNESS = 800f private const val DETACH_DAMPING_RATIO = 0.95f @@ -244,7 +270,8 @@ constructor( .setUsage(VibrationAttributes.USAGE_TOUCH) .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT) .build() - private const val MAX_VIBRATION_SCALE = 0.2f private const val VIBRATION_PERCEPTION_EXPONENT = 1 / 0.89f + private const val WEAK_VIBRATION_SCALE = 0.2f + private const val STRONG_VIBRATION_SCALE = 0.45f } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt index 8a1adfe95392..46036d4c1fad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt @@ -41,4 +41,7 @@ interface MagneticRowListener { /** Cancel any animations related to the magnetic interactions of the row */ fun cancelMagneticAnimations() + + /** Can the row be dismissed. */ + fun canRowBeDismissed(): Boolean } 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 ce1fc97cbffe..d6b34b068cc5 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 @@ -6609,10 +6609,7 @@ public class NotificationStackScrollLayout static boolean canChildBeDismissed(View v) { if (v instanceof ExpandableNotificationRow row) { - if (row.areGutsExposed() || !row.getEntry().hasFinishedInitialization()) { - return false; - } - return row.canViewBeDismissed(); + return row.canExpandableViewBeDismissed(); } return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt index 74e8b8ef29c2..b69b936ea9f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt @@ -3,7 +3,9 @@ package com.android.systemui.statusbar.notification.stack import androidx.core.view.children import androidx.core.view.isVisible import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.notification.Roundable +import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import javax.inject.Inject @@ -129,6 +131,10 @@ class NotificationTargetsHelper @Inject constructor() { magneticTargets[leftIndex] = leftElement.magneticRowListener leftIndex-- } else { + if (leftElement.isValidMagneticBoundary()) { + // Add the boundary and then stop the iterating + magneticTargets[leftIndex] = leftElement?.magneticRowListener + } canMoveLeft = false } } @@ -138,12 +144,24 @@ class NotificationTargetsHelper @Inject constructor() { magneticTargets[rightIndex] = rightElement.magneticRowListener rightIndex++ } else { + if (rightElement.isValidMagneticBoundary()) { + // Add the boundary and then stop the iterating + magneticTargets[rightIndex] = rightElement?.magneticRowListener + } canMoveRight = false } } } return magneticTargets } + + private fun ExpandableView?.isValidMagneticBoundary(): Boolean = + when (this) { + is FooterView, + is NotificationShelf, + is SectionHeaderView -> true + else -> false + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt index c8c798d00a06..5689230f6bed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack.ui.view import android.service.notification.NotificationListenerService import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.TrackTracer +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.statusbar.IStatusBarService import com.android.internal.statusbar.NotificationVisibility import com.android.systemui.dagger.SysUISingleton @@ -33,8 +34,9 @@ import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.withContext @VisibleForTesting const val UNKNOWN_RANK = -1 @@ -49,32 +51,56 @@ constructor( private val notificationPanelLogger: NotificationPanelLogger, private val statusBarService: IStatusBarService, ) : NotificationStatsLogger { - private val lastLoggedVisibilities = mutableMapOf<String, VisibilityState>() - private var logVisibilitiesJob: Job? = null - private val expansionStates: MutableMap<String, ExpansionState> = ConcurrentHashMap<String, ExpansionState>() @VisibleForTesting val lastReportedExpansionValues: MutableMap<String, Boolean> = ConcurrentHashMap<String, Boolean>() + private val visibilityLogger = + Channel<VisibilityAction>(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + init { + applicationScope.launch { consumeVisibilityActions() } + } + + private suspend fun consumeVisibilityActions() { + val lastLoggedVisibilities = mutableMapOf<String, VisibilityState>() + + visibilityLogger.consumeEach { action -> + val newVisibilities = + when (action) { + is VisibilityAction.Change -> action.visibilities + is VisibilityAction.Clear -> emptyMap() + } + + val newlyVisible = newVisibilities - lastLoggedVisibilities.keys + val noLongerVisible = lastLoggedVisibilities - newVisibilities.keys + + maybeLogVisibilityChanges(newlyVisible, noLongerVisible, action.activeCount) + updateExpansionStates(newlyVisible, noLongerVisible) + TrackTracer.instantForGroup("Notifications", "Active", action.activeCount) + TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size) + + lastLoggedVisibilities.clear() + lastLoggedVisibilities.putAll(newVisibilities) + } + } + override fun onNotificationLocationsChanged( locationsProvider: Callable<Map<String, Int>>, notificationRanks: Map<String, Int>, ) { - if (logVisibilitiesJob?.isActive == true) { - return - } - - logVisibilitiesJob = - startLogVisibilitiesJob( - newVisibilities = + visibilityLogger.trySend( + VisibilityAction.Change( + visibilities = combine( visibilities = locationsProvider.call(), - rankingsMap = notificationRanks + rankingsMap = notificationRanks, ), - activeNotifCount = notificationRanks.size, + activeCount = notificationRanks.size, ) + ) } override fun onNotificationExpansionChanged( @@ -125,7 +151,7 @@ constructor( /* expanded = */ expansionState.isExpanded, /* notificationLocation = */ expansionState.location .toNotificationLocation() - .ordinal + .ordinal, ) } } @@ -138,7 +164,7 @@ constructor( withContext(bgDispatcher) { notificationPanelLogger.logPanelShown( isOnLockScreen, - activeNotifications.toNotificationProto() + activeNotifications.toNotificationProto(), ) } } @@ -147,11 +173,7 @@ constructor( override fun onLockscreenOrShadeNotInteractive( activeNotifications: List<ActiveNotificationModel> ) { - logVisibilitiesJob = - startLogVisibilitiesJob( - newVisibilities = emptyMap(), - activeNotifCount = activeNotifications.size - ) + visibilityLogger.trySend(VisibilityAction.Clear(activeCount = activeNotifications.size)) } override fun onNotificationRemoved(key: String) { @@ -167,29 +189,12 @@ constructor( private fun combine( visibilities: Map<String, Int>, - rankingsMap: Map<String, Int> + rankingsMap: Map<String, Int>, ): Map<String, VisibilityState> = visibilities.mapValues { entry -> VisibilityState(entry.key, entry.value, rankingsMap[entry.key] ?: UNKNOWN_RANK) } - private fun startLogVisibilitiesJob( - newVisibilities: Map<String, VisibilityState>, - activeNotifCount: Int, - ) = - applicationScope.launch { - val newlyVisible = newVisibilities - lastLoggedVisibilities.keys - val noLongerVisible = lastLoggedVisibilities - newVisibilities.keys - - maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount) - updateExpansionStates(newlyVisible, noLongerVisible) - TrackTracer.instantForGroup("Notifications", "Active", activeNotifCount) - TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size) - - lastLoggedVisibilities.clear() - lastLoggedVisibilities.putAll(newVisibilities) - } - private suspend fun maybeLogVisibilityChanges( newlyVisible: Map<String, VisibilityState>, noLongerVisible: Map<String, VisibilityState>, @@ -205,7 +210,7 @@ constructor( val noLongerVisibleAr = noLongerVisible.mapToNotificationVisibilitiesAr( visible = false, - count = activeNotifCount + count = activeNotifCount, ) withContext(bgDispatcher) { @@ -218,7 +223,7 @@ constructor( private fun updateExpansionStates( newlyVisible: Map<String, VisibilityState>, - noLongerVisible: Map<String, VisibilityState> + noLongerVisible: Map<String, VisibilityState>, ) { expansionStates.forEach { (key, expansionState) -> if (newlyVisible.contains(key)) { @@ -241,11 +246,16 @@ constructor( } } - private data class VisibilityState( - val key: String, - val location: Int, - val rank: Int, - ) + private sealed class VisibilityAction(open val activeCount: Int) { + data class Change( + val visibilities: Map<String, VisibilityState>, + override val activeCount: Int, + ) : VisibilityAction(activeCount) + + data class Clear(override val activeCount: Int) : VisibilityAction(activeCount) + } + + private data class VisibilityState(val key: String, val location: Int, val rank: Int) private data class ExpansionState( val key: String, @@ -278,7 +288,7 @@ constructor( /* rank = */ state.rank, /* count = */ count, /* visible = */ visible, - /* location = */ state.location.toNotificationLocation() + /* location = */ state.location.toNotificationLocation(), ) } .toTypedArray() diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteria.kt deleted file mode 100644 index bee45645bfdb..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteria.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume.panel.component.mediaoutput.domain - -import android.content.Context -import com.android.settingslib.media.PhoneMediaDevice.isDesktop -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope -import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.stateIn - -@VolumePanelScope -class MediaOutputAvailabilityCriteria -@Inject -constructor( - @Application private val context: Context, - @VolumePanelScope private val scope: CoroutineScope, -) : ComponentAvailabilityCriteria { - - private val availability = - flow { emit(!isDesktop(context)) }.stateIn(scope, SharingStarted.WhileSubscribed(), false) - - override fun isAvailable(): Flow<Boolean> = availability -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt index 82e247714794..fbc6f84cb8fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt @@ -29,10 +29,10 @@ import com.android.internal.logging.InstanceId import com.android.internal.statusbar.IAddTileResultCallback import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.qs.QSHost import com.android.systemui.qs.external.ui.dialog.tileRequestDialogComposeDelegateFactory import com.android.systemui.qs.flags.QSComposeFragment -import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.testKosmos @@ -57,7 +57,8 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) -@DisableFlags(value = [QSComposeFragment.FLAG_NAME, DualShade.FLAG_NAME]) +@DisableFlags(QSComposeFragment.FLAG_NAME) +@DisableSceneContainer class TileServiceRequestControllerTestComposeOff : SysuiTestCase() { companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt index 50b8f37f8d25..c20a801cd5e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt @@ -63,7 +63,7 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) @EnableSceneContainer -@EnableFlags(Flags.FLAG_QS_TILE_DETAILED_VIEW, Flags.FLAG_DUAL_SHADE) +@EnableFlags(Flags.FLAG_QS_TILE_DETAILED_VIEW) @UiThreadTest class InternetDetailsContentManagerTest : SysuiTestCase() { private val kosmos = Kosmos() diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt index 69b762b470b7..40547c2787ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt @@ -90,7 +90,7 @@ class LauncherProxyServiceTest : SysuiTestCase() { private val kosmos = testKosmos() private lateinit var subject: LauncherProxyService @Mock private val dumpManager = DumpManager() - @Mock private val processWrapper = ProcessWrapper() + @Mock private lateinit var processWrapper: ProcessWrapper private val displayTracker = FakeDisplayTracker(mContext) private val fakeSystemClock = FakeSystemClock() private val sysUiState = SysUiState(displayTracker, kosmos.sceneContainerPlugin) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt index 57c28580c063..b75dd0402175 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt @@ -58,7 +58,8 @@ class SimpleDigitalClockTextViewTest : SysuiTestCase() { }, ClockMessageBuffers(messageBuffer), messageBuffer, - ) + ), + isLargeClock = false, ) underTest.textStyle = FontTextStyle() underTest.aodStyle = FontTextStyle() diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 097f3929db42..bf10dc6c4aef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -2110,7 +2110,7 @@ public class BubblesTest extends SysuiTestCase { assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(noteBubbleKey); assertThat(mBubbleController.isStackExpanded()).isTrue(); assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); - assertThat(mBubbleData.getBubbles().get(0).getAppBubbleIntent() + assertThat(mBubbleData.getBubbles().get(0).getIntent() .getStringExtra("hello")).isEqualTo("world"); assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNull(); } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 8ea80081a871..1952f26b4e6a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -114,6 +114,7 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override val keyguardAlpha: StateFlow<Float> = _keyguardAlpha override val panelAlpha: MutableStateFlow<Float> = MutableStateFlow(1f) + override val zoomOut: MutableStateFlow<Float> = MutableStateFlow(0f) override val lastRootViewTapPosition: MutableStateFlow<Point?> = MutableStateFlow(null) @@ -272,6 +273,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { panelAlpha.value = alpha } + override fun setZoomOut(zoomOutFromShadeRadius: Float) { + zoomOut.value = zoomOutFromShadeRadius + } + fun setIsEncryptedOrLockdown(value: Boolean) { _isEncryptedOrLockdown.value = value } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt index f47b2df607c1..78d44d4917fe 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt @@ -20,8 +20,8 @@ import com.android.systemui.biometrics.authController import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos -import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor @@ -34,7 +34,7 @@ val Kosmos.lockscreenContentViewModel by touchHandling = keyguardTouchHandlingViewModel, shadeInteractor = shadeInteractor, unfoldTransitionInteractor = unfoldTransitionInteractor, - occlusionInteractor = sceneContainerOcclusionInteractor, deviceEntryInteractor = deviceEntryInteractor, + transitionInteractor = keyguardTransitionInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt index 79167f840f60..4d1e0a8c025a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessKosmos.kt @@ -16,6 +16,9 @@ package com.android.systemui.process +import android.app.ActivityManager + import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock -val Kosmos.processWrapper: ProcessWrapperFake by Kosmos.Fixture { ProcessWrapperFake() } +val Kosmos.processWrapper: ProcessWrapperFake by Kosmos.Fixture { ProcessWrapperFake(mock<ActivityManager>()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt index dee3644e95bd..152cc3019d85 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/process/ProcessWrapperFake.kt @@ -16,9 +16,10 @@ package com.android.systemui.process +import android.app.ActivityManager import android.os.UserHandle -class ProcessWrapperFake : ProcessWrapper() { +class ProcessWrapperFake(activityManager: ActivityManager) : ProcessWrapper(activityManager) { var systemUser: Boolean = false diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt index de52155dce79..f91c2f620bb1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt @@ -26,7 +26,7 @@ import com.android.systemui.statusbar.notification.logging.notificationPanelLogg val Kosmos.notificationStatsLogger by Fixture { NotificationStatsLoggerImpl( - applicationScope = testScope, + applicationScope = testScope.backgroundScope, bgDispatcher = testDispatcher, statusBarService = statusBarService, notificationListenerService = notificationListenerService, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt index 556c34d85f8e..0c814c566d63 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModuleKosmos.kt @@ -16,16 +16,13 @@ package com.android.systemui.volume.panel.component.mediaoutput -import android.content.applicationContext import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testScope -import com.android.systemui.volume.panel.component.mediaoutput.domain.MediaOutputAvailabilityCriteria import com.android.systemui.volume.panel.component.mediaoutput.ui.composable.MediaOutputComponent import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.mediaOutputViewModel +import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import com.android.systemui.volume.panel.domain.availableCriteria var Kosmos.mediaOutputComponent: MediaOutputComponent by Kosmos.Fixture { MediaOutputComponent(mediaOutputViewModel) } -var Kosmos.mediaOutputAvailabilityCriteria by - Kosmos.Fixture { - MediaOutputAvailabilityCriteria(applicationContext, testScope.backgroundScope) - } +var Kosmos.mediaOutputAvailabilityCriteria: ComponentAvailabilityCriteria by + Kosmos.Fixture { availableCriteria } diff --git a/services/core/Android.bp b/services/core/Android.bp index 420dcfe9cea6..9b0caf561544 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -227,6 +227,7 @@ java_library_static { "com.android.sysprop.watchdog", "securebox", "apache-commons-math", + "apache-commons-compress", "battery_saver_flag_lib", "notification_flags_lib", "power_hint_flags_lib", diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java index 626fa708b4e7..7e68239e0c3b 100644 --- a/services/core/java/com/android/server/VpnManagerService.java +++ b/services/core/java/com/android/server/VpnManagerService.java @@ -19,6 +19,7 @@ package com.android.server; import static android.Manifest.permission.NETWORK_STACK; import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf; +import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1020,6 +1021,8 @@ public class VpnManagerService extends IVpnManager.Stub { @Override @Nullable public byte[] getFromVpnProfileStore(@NonNull String name) { + // TODO(b/307903113): Replace NETWORK_STACK permission and adopt proper permission + enforceNetworkStackPermission(mContext); return mVpnProfileStore.get(name); } @@ -1037,6 +1040,8 @@ public class VpnManagerService extends IVpnManager.Stub { */ @Override public boolean putIntoVpnProfileStore(@NonNull String name, @NonNull byte[] blob) { + // TODO(b/307903113): Replace NETWORK_STACK permission and adopt proper permission + enforceNetworkStackPermission(mContext); return mVpnProfileStore.put(name, blob); } @@ -1052,6 +1057,8 @@ public class VpnManagerService extends IVpnManager.Stub { */ @Override public boolean removeFromVpnProfileStore(@NonNull String name) { + // TODO(b/307903113): Replace NETWORK_STACK permission and adopt proper permission + enforceNetworkStackPermission(mContext); return mVpnProfileStore.remove(name); } @@ -1069,6 +1076,8 @@ public class VpnManagerService extends IVpnManager.Stub { @Override @NonNull public String[] listFromVpnProfileStore(@NonNull String prefix) { + // TODO(b/307903113): Replace NETWORK_STACK permission and adopt proper permission + enforceNetworkStackPermission(mContext); return mVpnProfileStore.list(prefix); } diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index 8e520dc632c3..96b30d4e1285 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -191,6 +191,9 @@ public class Watchdog implements Dumpable { "android.hardware.sensors.ISensors/", "android.hardware.vibrator.IVibrator/", "android.hardware.vibrator.IVibratorManager/", + "android.hardware.wifi.hostapd.IHostapd/", + "android.hardware.wifi.IWifi/", + "android.hardware.wifi.supplicant.ISupplicant/", "android.system.suspend.ISystemSuspend/", }; diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java index 374abe0256c1..0bc816e78e7b 100644 --- a/services/core/java/com/android/server/am/AppBatteryTracker.java +++ b/services/core/java/com/android/server/am/AppBatteryTracker.java @@ -818,8 +818,10 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> void dump(PrintWriter pw, String prefix) { pw.print(prefix); pw.println("APP BATTERY STATE TRACKER:"); - // Force an update. - updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true); + if (mInjector.getActivityManagerInternal().isBooted()) { + // Force an update. + updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true); + } // Force a check. scheduleBgBatteryUsageStatsCheck(); // Wait for its completion (as it runs in handler thread for the sake of thread safe) @@ -878,8 +880,10 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> @Override void dumpAsProto(ProtoOutputStream proto, int uid) { - // Force an update. - updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true); + if (mInjector.getActivityManagerInternal().isBooted()) { + // Force an update. + updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true); + } synchronized (mLock) { final SparseArray<ImmutableBatteryUsage> uidConsumers = mUidBatteryUsageInWindow; if (uid != android.os.Process.INVALID_UID) { diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 644077a7e6bb..c8b0a57fe9f0 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -526,6 +526,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub } public void systemServicesReady() { + mStats.setBatteryHistoryCompressionEnabled( + Flags.extendedBatteryHistoryCompressionEnabled()); mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore, isBatteryUsageStatsAccumulationSupported()); mStats.resetBatteryHistoryOnNewSession( diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java index 740c4f195852..4389dd09ba73 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java @@ -30,6 +30,7 @@ import android.os.ServiceSpecificException; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import java.util.Collections; import java.util.HashMap; @@ -45,14 +46,14 @@ import java.util.function.Consumer; */ /* package */ class ContextHubEndpointManager implements ContextHubHalEndpointCallback.IEndpointSessionCallback { + /** The range of session IDs to use for endpoints */ + public static final int SERVICE_SESSION_RANGE = 1024; + private static final String TAG = "ContextHubEndpointManager"; /** The hub ID of the Context Hub Service. */ private static final long SERVICE_HUB_ID = 0x416e64726f696400L; - /** The range of session IDs to use for endpoints */ - private static final int SERVICE_SESSION_RANGE = 1024; - /** The length of the array that should be returned by HAL requestSessionIdRange */ private static final int SERVICE_SESSION_RANGE_LENGTH = 2; @@ -400,4 +401,16 @@ import java.util.function.Consumer; private boolean isSessionIdRangeValid(int minId, int maxId) { return (minId <= maxId) && (minId >= 0) && (maxId >= 0); } + + @VisibleForTesting + /* package */ int getNumAvailableSessions() { + synchronized (mSessionIdLock) { + return (mMaxSessionId - mMinSessionId + 1) - mReservedSessionIds.size(); + } + } + + @VisibleForTesting + /* package */ int getNumRegisteredClients() { + return mEndpointMap.size(); + } } diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index d00ac4d9cd11..c93f107d6640 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -1160,6 +1160,9 @@ public class MediaQualityService extends SystemService { private Bundle convertToCaps(ParameterRange range) { Bundle bundle = new Bundle(); + if (range == null || range.numRange == null) { + return bundle; + } bundle.putObject("INT_MIN_MAX", range.numRange.getIntMinMax()); bundle.putObject("INT_VALUES_SUPPORTED", range.numRange.getIntValuesSupported()); bundle.putObject("DOUBLE_MIN_MAX", range.numRange.getDoubleMinMax()); @@ -1351,7 +1354,7 @@ public class MediaQualityService extends SystemService { RemoteCallbackList<IPictureProfileCallback> { @Override public void onCallbackDied(IPictureProfileCallback callback) { - synchronized ("mPictureProfileLock") { //TODO: Change to lock + synchronized (mPictureProfileLock) { for (int i = 0; i < mUserStates.size(); i++) { int userId = mUserStates.keyAt(i); UserState userState = getOrCreateUserStateLocked(userId); @@ -1365,7 +1368,7 @@ public class MediaQualityService extends SystemService { RemoteCallbackList<ISoundProfileCallback> { @Override public void onCallbackDied(ISoundProfileCallback callback) { - synchronized ("mSoundProfileLock") { //TODO: Change to lock + synchronized (mSoundProfileLock) { for (int i = 0; i < mUserStates.size(); i++) { int userId = mUserStates.keyAt(i); UserState userState = getOrCreateUserStateLocked(userId); diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java index 5bd4420e9944..ba61e508bef6 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java @@ -357,6 +357,9 @@ public final class MediaQualityUtils { */ public static PictureParameter[] convertPersistableBundleToPictureParameterList( PersistableBundle params) { + if (params == null) { + return null; + } List<PictureParameter> pictureParams = new ArrayList<>(); if (params.containsKey(PictureQuality.PARAMETER_BRIGHTNESS)) { pictureParams.add(PictureParameter.brightness(params.getLong( @@ -784,6 +787,9 @@ public final class MediaQualityUtils { */ public static SoundParameter[] convertPersistableBundleToSoundParameterList( PersistableBundle params) { + if (params == null) { + return null; + } //TODO: set EqualizerDetail List<SoundParameter> soundParams = new ArrayList<>(); if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) { diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 7de7dde8c260..c7737e9f8bbd 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -129,6 +129,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.os.UserManager.EnforcingUser; import android.os.UserManager.QuietModeFlag; +import android.os.UserManager.UserLogoutability; import android.os.storage.StorageManager; import android.os.storage.StorageManagerInternal; import android.provider.Settings; @@ -1472,10 +1473,13 @@ public class UserManagerService extends IUserManager.Stub { return UserHandle.USER_NULL; } - @Override public @CanBeNULL @UserIdInt int getPreviousFullUserToEnterForeground() { checkQueryOrCreateUsersPermission("get previous user"); + return getPreviousFullUserToEnterForegroundUnchecked(); + } + + private int getPreviousFullUserToEnterForegroundUnchecked() { int previousUser = UserHandle.USER_NULL; long latestEnteredTime = 0; final int currentUser = getCurrentUserId(); @@ -2915,7 +2919,8 @@ public class UserManagerService extends IUserManager.Stub { * @return A {@link UserManager.UserSwitchabilityResult} flag indicating if the user is * switchable. */ - public @UserManager.UserSwitchabilityResult int getUserSwitchability(int userId) { + @Override + public @UserManager.UserSwitchabilityResult int getUserSwitchability(@UserIdInt int userId) { if (Flags.getUserSwitchabilityPermission()) { if (!hasManageUsersOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)) { throw new SecurityException( @@ -2994,6 +2999,49 @@ public class UserManagerService extends IUserManager.Stub { } @Override + public @UserLogoutability int getUserLogoutability(@UserIdInt int userId) { + if (!android.multiuser.Flags.logoutUserApi()) { + throw new UnsupportedOperationException( + "aconfig flag android.multiuser.logout_user_api not enabled"); + } + + checkManageUsersPermission("getUserLogoutability"); + + if (userId == UserHandle.USER_SYSTEM) { + return UserManager.LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER; + } + + if (userId != getCurrentUserId()) { + // TODO(b/393656514): Decide what to do with non-current/background users. + // As of now, we are not going to logout a background user. A background user should + // simply be stopped instead. + return UserManager.LOGOUTABILITY_STATUS_CANNOT_SWITCH; + } + + if (getUserSwitchability(userId) != UserManager.SWITCHABILITY_STATUS_OK) { + return UserManager.LOGOUTABILITY_STATUS_CANNOT_SWITCH; + } + + if (getUserToLogoutCurrentUserTo() == UserHandle.USER_NULL) { + return UserManager.LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO; + } + + return UserManager.LOGOUTABILITY_STATUS_OK; + } + + /** + * Returns the user to switch to, when logging out current user. If in HSUM and has interactive + * system user, then logout would switch to the system user. Otherwise, logout would switch to + * the previous foreground user. + */ + private @UserIdInt int getUserToLogoutCurrentUserTo() { + if (isHeadlessSystemUserMode() && canSwitchToHeadlessSystemUser()) { + return USER_SYSTEM; + } + return getPreviousFullUserToEnterForegroundUnchecked(); + } + + @Override public boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable, @UserIdInt int userId) { if (!isUserSwitcherEnabled(userId)) { diff --git a/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java new file mode 100644 index 000000000000..adf308a522ed --- /dev/null +++ b/services/core/java/com/android/server/power/stats/BatteryHistoryDirectory.java @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.power.stats; + +import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; + +import android.annotation.NonNull; +import android.os.SystemClock; +import android.os.Trace; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BackgroundThread; +import com.android.internal.os.BatteryStatsHistory; +import com.android.internal.os.BatteryStatsHistory.BatteryHistoryFragment; + +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipParameters; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; +import java.util.zip.Deflater; +import java.util.zip.GZIPInputStream; + +public class BatteryHistoryDirectory implements BatteryStatsHistory.BatteryHistoryStore { + public static final String TAG = "BatteryHistoryDirectory"; + private static final boolean DEBUG = false; + + private static final String FILE_SUFFIX = ".bh"; + + // Size of the magic number written at the start of each history file + private static final int FILE_FORMAT_BYTES = 4; + private static final byte[] FILE_FORMAT_PARCEL = {0x50, 0x52, 0x43, 0x4c}; // PRCL + private static final byte[] FILE_FORMAT_COMPRESSED_PARCEL = {0x47, 0x5a, 0x49, 0x50}; // GZIP + + static class BatteryHistoryFile extends BatteryHistoryFragment { + public final AtomicFile atomicFile; + + BatteryHistoryFile(File directory, long monotonicTimeMs) { + super(monotonicTimeMs); + atomicFile = new AtomicFile(new File(directory, monotonicTimeMs + FILE_SUFFIX)); + } + + @Override + public String toString() { + return atomicFile.getBaseFile().toString(); + } + } + + interface Compressor { + void compress(OutputStream stream, byte[] data) throws IOException; + void uncompress(byte[] data, InputStream stream) throws IOException; + + default void readFully(byte[] data, InputStream stream) throws IOException { + int pos = 0; + while (pos < data.length) { + int count = stream.read(data, pos, data.length - pos); + if (count == -1) { + throw new IOException("Invalid battery history file format"); + } + pos += count; + } + } + } + + static final Compressor DEFAULT_COMPRESSOR = new Compressor() { + @Override + public void compress(OutputStream stream, byte[] data) throws IOException { + // With the BEST_SPEED hint, we see ~4x improvement in write latency over + // GZIPOutputStream. + GzipParameters parameters = new GzipParameters(); + parameters.setCompressionLevel(Deflater.BEST_SPEED); + GzipCompressorOutputStream os = new GzipCompressorOutputStream(stream, parameters); + os.write(data); + os.finish(); + os.flush(); + } + + @Override + public void uncompress(byte[] data, InputStream stream) throws IOException { + readFully(data, new GZIPInputStream(stream)); + } + }; + + private final File mDirectory; + private int mMaxHistorySize; + private boolean mInitialized; + private final List<BatteryHistoryFile> mHistoryFiles = new ArrayList<>(); + private final ReentrantLock mLock = new ReentrantLock(); + private final Compressor mCompressor; + private boolean mWaitForDirectoryLock = false; + private boolean mFileCompressionEnabled; + + public BatteryHistoryDirectory(@NonNull File directory, int maxHistorySize) { + this(directory, maxHistorySize, DEFAULT_COMPRESSOR); + } + + public BatteryHistoryDirectory(@NonNull File directory, int maxHistorySize, + Compressor compressor) { + mDirectory = directory; + mMaxHistorySize = maxHistorySize; + if (mMaxHistorySize == 0) { + Slog.w(TAG, "maxHistorySize should not be zero"); + } + mCompressor = compressor; + } + + public void setFileCompressionEnabled(boolean enabled) { + mFileCompressionEnabled = enabled; + } + + void setMaxHistorySize(int maxHistorySize) { + mMaxHistorySize = maxHistorySize; + trim(); + } + + /** + * Returns the maximum storage size allocated to battery history. + */ + public int getMaxHistorySize() { + return mMaxHistorySize; + } + + @Override + public void lock() { + mLock.lock(); + } + + /** + * Turns "tryLock" into "lock" to prevent flaky unit tests. + * Should only be called from unit tests. + */ + @VisibleForTesting + void makeDirectoryLockUnconditional() { + mWaitForDirectoryLock = true; + } + + @Override + public boolean tryLock() { + if (mWaitForDirectoryLock) { + mLock.lock(); + return true; + } + return mLock.tryLock(); + } + + @Override + public void writeFragment(BatteryHistoryFragment fragment, + @NonNull byte[] data, boolean fragmentComplete) { + AtomicFile file = ((BatteryHistoryFile) fragment).atomicFile; + FileOutputStream fos = null; + try { + final long startTimeMs = SystemClock.uptimeMillis(); + fos = file.startWrite(); + fos.write(FILE_FORMAT_PARCEL); + writeInt(fos, data.length); + fos.write(data); + fos.flush(); + file.finishWrite(fos); + if (DEBUG) { + Slog.d(TAG, "writeHistoryFragment file:" + file.getBaseFile().getPath() + + " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs) + + " bytes:" + data.length); + } + if (fragmentComplete) { + if (mFileCompressionEnabled) { + BackgroundThread.getHandler().post( + () -> writeHistoryFragmentCompressed(file, data)); + } + BackgroundThread.getHandler().post(()-> trim()); + } + } catch (IOException e) { + Slog.w(TAG, "Error writing battery history fragment", e); + file.failWrite(fos); + } + } + + private void writeHistoryFragmentCompressed(AtomicFile file, byte[] data) { + long uncompressedSize = data.length; + if (uncompressedSize == 0) { + return; + } + + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.compressHistoryFile"); + lock(); + FileOutputStream fos = null; + try { + long startTimeNs = System.nanoTime(); + fos = file.startWrite(); + fos.write(FILE_FORMAT_COMPRESSED_PARCEL); + writeInt(fos, data.length); + + mCompressor.compress(fos, data); + file.finishWrite(fos); + + if (DEBUG) { + long endTimeNs = System.nanoTime(); + long compressedSize = file.getBaseFile().length(); + Slog.i(TAG, String.format(Locale.ENGLISH, + "Compressed battery history file %s original size: %d compressed: %d " + + "(%.1f%%) elapsed: %.2f ms", + file.getBaseFile(), uncompressedSize, compressedSize, + (uncompressedSize - compressedSize) * 100.0 / uncompressedSize, + (endTimeNs - startTimeNs) / 1000000.0)); + } + } catch (Exception e) { + Slog.w(TAG, "Error compressing battery history chunk " + file, e); + file.failWrite(fos); + } finally { + unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + + @Override + public byte[] readFragment(BatteryHistoryFragment fragment) { + AtomicFile file = ((BatteryHistoryFile) fragment).atomicFile; + if (!file.exists()) { + deleteFragment(fragment); + return null; + } + final long start = SystemClock.uptimeMillis(); + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read"); + try (FileInputStream stream = file.openRead()) { + byte[] header = new byte[FILE_FORMAT_BYTES]; + if (stream.read(header, 0, FILE_FORMAT_BYTES) == -1) { + Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); + deleteFragment(fragment); + return null; + } + + boolean isCompressed; + if (Arrays.equals(header, FILE_FORMAT_COMPRESSED_PARCEL)) { + isCompressed = true; + } else if (Arrays.equals(header, FILE_FORMAT_PARCEL)) { + isCompressed = false; + } else { + Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); + deleteFragment(fragment); + return null; + } + + int size = readInt(stream); + if (size < 0 || size > 10000000) { // Validity check to avoid a crash + Slog.e(TAG, "Invalid battery history file format " + file.getBaseFile()); + deleteFragment(fragment); + return null; + } + + byte[] data = new byte[size]; + if (isCompressed) { + mCompressor.uncompress(data, stream); + } else { + int pos = 0; + while (pos < data.length) { + int count = stream.read(data, pos, data.length - pos); + if (count == -1) { + throw new IOException("Invalid battery history file format"); + } + pos += count; + } + } + if (DEBUG) { + Slog.d(TAG, "readHistoryFragment:" + file.getBaseFile().getPath() + + " duration ms:" + (SystemClock.uptimeMillis() - start)); + } + return data; + } catch (Exception e) { + Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); + deleteFragment(fragment); + return null; + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + + private void deleteFragment(BatteryHistoryFragment fragment) { + mHistoryFiles.remove(fragment); + ((BatteryHistoryFile) fragment).atomicFile.delete(); + } + + @Override + public void unlock() { + mLock.unlock(); + } + + @Override + public boolean isLocked() { + return mLock.isLocked(); + } + + private void ensureInitialized() { + if (mInitialized) { + return; + } + + Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); + mDirectory.mkdirs(); + if (!mDirectory.exists()) { + Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath()); + } + + final List<File> toRemove = new ArrayList<>(); + final Set<BatteryHistoryFile> dedup = new ArraySet<>(); + mDirectory.listFiles((dir, name) -> { + final int b = name.lastIndexOf(FILE_SUFFIX); + if (b <= 0) { + toRemove.add(new File(dir, name)); + return false; + } + try { + long monotonicTime = Long.parseLong(name.substring(0, b)); + dedup.add(new BatteryHistoryFile(mDirectory, monotonicTime)); + } catch (NumberFormatException e) { + toRemove.add(new File(dir, name)); + return false; + } + return true; + }); + if (!dedup.isEmpty()) { + mHistoryFiles.addAll(dedup); + Collections.sort(mHistoryFiles); + } + mInitialized = true; + if (!toRemove.isEmpty()) { + // Clear out legacy history files, which did not follow the X-Y.bin naming format. + BackgroundThread.getHandler().post(() -> { + lock(); + try { + for (File file : toRemove) { + file.delete(); + } + } finally { + unlock(); + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); + } + }); + } else { + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); + } + } + + @SuppressWarnings("unchecked") + @Override + public List<BatteryHistoryFragment> getFragments() { + ensureInitialized(); + return (List<BatteryHistoryFragment>) + (List<? extends BatteryHistoryFragment>) mHistoryFiles; + } + + @VisibleForTesting + List<String> getFileNames() { + ensureInitialized(); + lock(); + try { + List<String> names = new ArrayList<>(); + for (BatteryHistoryFile historyFile : mHistoryFiles) { + names.add(historyFile.atomicFile.getBaseFile().getName()); + } + return names; + } finally { + unlock(); + } + } + + @Override + public BatteryHistoryFragment getEarliestFragment() { + ensureInitialized(); + lock(); + try { + if (!mHistoryFiles.isEmpty()) { + return mHistoryFiles.get(0); + } + return null; + } finally { + unlock(); + } + } + + @Override + public BatteryHistoryFragment getLatestFragment() { + ensureInitialized(); + lock(); + try { + if (!mHistoryFiles.isEmpty()) { + return mHistoryFiles.get(mHistoryFiles.size() - 1); + } + return null; + } finally { + unlock(); + } + } + + @Override + public BatteryHistoryFragment createFragment(long monotonicStartTime) { + ensureInitialized(); + + BatteryHistoryFile file = new BatteryHistoryFile(mDirectory, monotonicStartTime); + lock(); + try { + try { + file.atomicFile.getBaseFile().createNewFile(); + } catch (IOException e) { + Slog.e(TAG, "Could not create history file: " + file); + } + mHistoryFiles.add(file); + } finally { + unlock(); + } + + return file; + } + + @Override + public BatteryHistoryFragment getNextFragment(BatteryHistoryFragment current, long startTimeMs, + long endTimeMs) { + ensureInitialized(); + + if (!mLock.isHeldByCurrentThread()) { + throw new IllegalStateException("Iterating battery history without a lock"); + } + + int nextFileIndex = 0; + int firstFileIndex = 0; + // skip the last file because its data is in history buffer. + int lastFileIndex = mHistoryFiles.size() - 2; + for (int i = lastFileIndex; i >= 0; i--) { + BatteryHistoryFragment fragment = mHistoryFiles.get(i); + if (current != null && fragment.monotonicTimeMs == current.monotonicTimeMs) { + nextFileIndex = i + 1; + } + if (fragment.monotonicTimeMs > endTimeMs) { + lastFileIndex = i - 1; + } + if (fragment.monotonicTimeMs <= startTimeMs) { + firstFileIndex = i; + break; + } + } + + if (nextFileIndex < firstFileIndex) { + nextFileIndex = firstFileIndex; + } + + if (nextFileIndex <= lastFileIndex) { + return mHistoryFiles.get(nextFileIndex); + } + + return null; + } + + @Override + public boolean hasCompletedFragments() { + ensureInitialized(); + + lock(); + try { + // Active file is partial and does not count as "competed" + return mHistoryFiles.size() > 1; + } finally { + unlock(); + } + } + + @Override + public int getSize() { + ensureInitialized(); + + lock(); + try { + int ret = 0; + for (int i = 0; i < mHistoryFiles.size() - 1; i++) { + ret += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); + } + return ret; + } finally { + unlock(); + } + } + + @Override + public void reset() { + ensureInitialized(); + + lock(); + try { + if (DEBUG) { + Slog.i(TAG, "********** CLEARING HISTORY!"); + } + for (BatteryHistoryFile file : mHistoryFiles) { + file.atomicFile.delete(); + } + mHistoryFiles.clear(); + } finally { + unlock(); + } + } + + private void trim() { + ensureInitialized(); + + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.trim"); + try { + lock(); + try { + // if there is more history stored than allowed, delete oldest history files. + int size = 0; + for (int i = 0; i < mHistoryFiles.size(); i++) { + size += (int) mHistoryFiles.get(i).atomicFile.getBaseFile().length(); + } + while (size > mMaxHistorySize) { + BatteryHistoryFile oldest = mHistoryFiles.get(0); + int length = (int) oldest.atomicFile.getBaseFile().length(); + oldest.atomicFile.delete(); + mHistoryFiles.remove(0); + size -= length; + } + } finally { + unlock(); + } + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); + } + } + + private static void writeInt(OutputStream stream, int value) throws IOException { + stream.write(value >> 24); + stream.write(value >> 16); + stream.write(value >> 8); + stream.write(value >> 0); + } + + private static int readInt(InputStream stream) throws IOException { + return (readByte(stream) << 24) + | (readByte(stream) << 16) + | (readByte(stream) << 8) + | (readByte(stream) << 0); + } + + private static int readByte(InputStream stream) throws IOException { + int out = stream.read(); + if (out == -1) { + throw new IOException(); + } + return out; + } +} diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 68768b8fa223..90bc54b06c0a 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -195,6 +195,8 @@ public class BatteryStatsImpl extends BatteryStats { private static final boolean DEBUG_BINDER_STATS = false; private static final boolean DEBUG_MEMORY = false; + private static final String HISTORY_DIR = "battery-history"; + // TODO: remove "tcp" from network methods, since we measure total stats. // Current on-disk Parcel version. Must be updated when the format of the parcelable changes @@ -1143,6 +1145,8 @@ public class BatteryStatsImpl extends BatteryStats { private int mBatteryTemperature; private int mBatteryVoltageMv; + @Nullable + private final BatteryHistoryDirectory mBatteryHistoryDirectory; @NonNull private final BatteryStatsHistory mHistory; @@ -11476,7 +11480,10 @@ public class BatteryStatsImpl extends BatteryStats { @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile, @NonNull CpuScalingPolicies cpuScalingPolicies, @NonNull PowerStatsUidResolver powerStatsUidResolver) { - this(config, clock, monotonicClock, systemDir, handler, platformIdleStateCallback, + this(config, clock, monotonicClock, systemDir, + systemDir != null ? new BatteryHistoryDirectory(new File(systemDir, HISTORY_DIR), + config.getMaxHistorySizeBytes()) : null, + handler, platformIdleStateCallback, energyStatsRetriever, userInfoProvider, powerProfile, cpuScalingPolicies, powerStatsUidResolver, new FrameworkStatsLogger(), new BatteryStatsHistory.TraceDelegate(), new BatteryStatsHistory.EventLogger()); @@ -11484,6 +11491,7 @@ public class BatteryStatsImpl extends BatteryStats { public BatteryStatsImpl(@NonNull BatteryStatsConfig config, @NonNull Clock clock, @NonNull MonotonicClock monotonicClock, @Nullable File systemDir, + @Nullable BatteryHistoryDirectory batteryHistoryDirectory, @NonNull Handler handler, @Nullable PlatformIdleStateCallback platformIdleStateCallback, @Nullable EnergyStatsRetriever energyStatsRetriever, @NonNull UserInfoProvider userInfoProvider, @NonNull PowerProfile powerProfile, @@ -11517,9 +11525,10 @@ public class BatteryStatsImpl extends BatteryStats { mDailyFile = null; } - mHistory = new BatteryStatsHistory(null /* historyBuffer */, systemDir, - mConstants.MAX_HISTORY_SIZE, mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, - mClock, mMonotonicClock, traceDelegate, eventLogger); + mBatteryHistoryDirectory = batteryHistoryDirectory; + mHistory = new BatteryStatsHistory(null /* historyBuffer */, mConstants.MAX_HISTORY_BUFFER, + mBatteryHistoryDirectory, mStepDetailsCalculator, mClock, mMonotonicClock, + traceDelegate, eventLogger); mCpuPowerStatsCollector = new CpuPowerStatsCollector(mPowerStatsCollectorInjector); mCpuPowerStatsCollector.addConsumer(this::recordPowerStats); @@ -12060,7 +12069,7 @@ public class BatteryStatsImpl extends BatteryStats { } public int getHistoryTotalSize() { - return mHistory.getMaxHistorySize(); + return mBatteryHistoryDirectory.getMaxHistorySize(); } public int getHistoryUsedSize() { @@ -12160,6 +12169,13 @@ public class BatteryStatsImpl extends BatteryStats { mResetBatteryHistoryOnNewSession = enabled; } + /** + * Enables or disables battery history file compression. + */ + public void setBatteryHistoryCompressionEnabled(boolean enabled) { + mBatteryHistoryDirectory.setFileCompressionEnabled(enabled); + } + @GuardedBy("this") public void resetAllStatsAndHistoryLocked(int reason) { final long mSecUptime = mClock.uptimeMillis(); @@ -16354,7 +16370,9 @@ public class BatteryStatsImpl extends BatteryStats { */ @VisibleForTesting public void onChange() { - mHistory.setMaxHistorySize(MAX_HISTORY_SIZE); + if (mBatteryHistoryDirectory != null) { + mBatteryHistoryDirectory.setMaxHistorySize(MAX_HISTORY_SIZE); + } mHistory.setMaxHistoryBufferSize(MAX_HISTORY_BUFFER); } diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig index c8dbbd29823c..521ee58decea 100644 --- a/services/core/java/com/android/server/power/stats/flags.aconfig +++ b/services/core/java/com/android/server/power/stats/flags.aconfig @@ -97,3 +97,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "extended_battery_history_compression_enabled" + namespace: "backstage_power" + description: "Compress each battery history chunk on disk" + bug: "381937912" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 6f76618b0029..247264f049d6 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -3035,6 +3035,12 @@ class ActivityStarter { } } + if (com.android.window.flags.Flags.fixLayoutExistingTask()) { + // Layout the task to ensure the Task is in correct bounds. + mSupervisor.getLaunchParamsController().layoutTask(intentTask, + mStartActivity.info.windowLayout, mStartActivity, mSourceRecord, mOptions); + } + // If the target task is not in the front, then we need to bring it to the front. final boolean differentTopTask; if (mTargetRootTask.getDisplayArea() == mPreferredTaskDisplayArea) { diff --git a/services/core/java/com/android/server/wm/AppCompatConfiguration.java b/services/core/java/com/android/server/wm/AppCompatConfiguration.java index 9a15c4a8bff2..0d8950b6cc45 100644 --- a/services/core/java/com/android/server/wm/AppCompatConfiguration.java +++ b/services/core/java/com/android/server/wm/AppCompatConfiguration.java @@ -311,7 +311,7 @@ final class AppCompatConfiguration { // Whether should ignore app requested orientation in response to an app // calling Activity#setRequestedOrientation. See - // LetterboxUiController#shouldIgnoreRequestedOrientation for details. + // AppCompatOrientationPolicy#shouldIgnoreRequestedOrientation for details. private final boolean mIsPolicyForIgnoringRequestedOrientationEnabled; // Flags dynamically updated with {@link android.provider.DeviceConfig}. @@ -1259,7 +1259,7 @@ final class AppCompatConfiguration { /** * Whether should ignore app requested orientation in response to an app calling * {@link android.app.Activity#setRequestedOrientation}. See {@link - * LetterboxUiController#shouldIgnoreRequestedOrientation} for details. + * AppCompatOrientationPolicy#shouldIgnoreRequestedOrientation} for details. */ boolean isPolicyForIgnoringRequestedOrientationEnabled() { return mIsPolicyForIgnoringRequestedOrientationEnabled; diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java index a7c52bd1fc38..b47786675fc9 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java @@ -32,6 +32,7 @@ import android.annotation.Nullable; import android.graphics.Rect; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.util.function.Supplier; @@ -97,8 +98,11 @@ class AppCompatReachabilityPolicy { private void handleHorizontalDoubleTap(int x) { final AppCompatReachabilityOverrides reachabilityOverrides = mActivityRecord.mAppCompatController.getReachabilityOverrides(); - if (!reachabilityOverrides.isHorizontalReachabilityEnabled() - || mActivityRecord.isInTransition()) { + // We don't return early when the Shell letterbox implementation is enabled because + // double tap is always sent via transitions. + final boolean isInTransition = !Flags.appCompatRefactoring() + && mActivityRecord.isInTransition(); + if (!reachabilityOverrides.isHorizontalReachabilityEnabled() || isInTransition) { return; } final Rect letterboxInnerFrame = getLetterboxInnerFrame(); @@ -143,8 +147,11 @@ class AppCompatReachabilityPolicy { private void handleVerticalDoubleTap(int y) { final AppCompatReachabilityOverrides reachabilityOverrides = mActivityRecord.mAppCompatController.getReachabilityOverrides(); - if (!reachabilityOverrides.isVerticalReachabilityEnabled() - || mActivityRecord.isInTransition()) { + // We don't return early when the Shell letterbox implementation is enabled because + // double tap is always sent via transitions. + final boolean isInTransition = !Flags.appCompatRefactoring() + && mActivityRecord.isInTransition(); + if (!reachabilityOverrides.isVerticalReachabilityEnabled() || isInTransition) { return; } final Rect letterboxInnerFrame = getLetterboxInnerFrame(); diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index 548addbef39d..ac987929a142 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -74,6 +74,12 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { appendLog("task null, skipping"); return RESULT_SKIP; } + if (com.android.window.flags.Flags.fixLayoutExistingTask() + && task.getOrganizedTask() != null) { + appendLog("task is organized, skipping"); + return RESULT_SKIP; + } + if (!task.isActivityTypeStandardOrUndefined()) { appendLog("not standard or undefined activity type, skipping"); return RESULT_SKIP; diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java index 4f5c0c8ecf6e..fa65bda7104d 100644 --- a/services/core/java/com/android/server/wm/LaunchParamsController.java +++ b/services/core/java/com/android/server/wm/LaunchParamsController.java @@ -124,31 +124,19 @@ class LaunchParamsController { } } - /** - * A convenience method for laying out a task. - * @return {@code true} if bounds were set on the task. {@code false} otherwise. - */ - boolean layoutTask(Task task, WindowLayout layout) { - return layoutTask(task, layout, null /*activity*/, null /*source*/, null /*options*/); - } - + /** @return {@code true} if bounds were set on the task. {@code false} otherwise. */ boolean layoutTask(Task task, WindowLayout layout, ActivityRecord activity, ActivityRecord source, ActivityOptions options) { calculate(task, layout, activity, source, options, null /* request */, PHASE_BOUNDS, mTmpParams); // No changes, return. - if (mTmpParams.isEmpty()) { + if (mTmpParams.isEmpty() || mTmpParams.mBounds.isEmpty()) { return false; } mService.deferWindowLayout(); - try { - if (mTmpParams.mBounds.isEmpty()) { - return false; - } - if (task.getRootTask().inMultiWindowMode()) { task.setBounds(mTmpParams.mBounds); return true; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a8686d3e4ea7..4b07e9e232be 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5232,7 +5232,6 @@ class Task extends TaskFragment { final boolean[] resumed = new boolean[1]; final TaskFragment topFragment = topActivity.getTaskFragment(); - resumed[0] = topFragment.resumeTopActivity(prev, options, deferPause); forAllLeafTaskFragments(f -> { if (topFragment == f) { return; @@ -5242,6 +5241,7 @@ class Task extends TaskFragment { } resumed[0] |= f.resumeTopActivity(prev, options, deferPause); }, true); + resumed[0] |= topFragment.resumeTopActivity(prev, options, deferPause); return resumed[0]; } @@ -6033,7 +6033,7 @@ class Task extends TaskFragment { IVoiceInteractor voiceInteractor, boolean toTop, ActivityRecord activity, ActivityRecord source, ActivityOptions options) { - Task task; + final Task task; if (canReuseAsLeafTask()) { // This root task will only contain one task, so just return itself since all root // tasks ara now tasks and all tasks are now root tasks. @@ -6043,7 +6043,6 @@ class Task extends TaskFragment { final int taskId = activity != null ? mTaskSupervisor.getNextTaskIdForUser(activity.mUserId) : mTaskSupervisor.getNextTaskIdForUser(); - final int activityType = getActivityType(); task = new Task.Builder(mAtmService) .setTaskId(taskId) .setActivityInfo(info) @@ -6056,17 +6055,21 @@ class Task extends TaskFragment { .build(); } - int displayId = getDisplayId(); - if (displayId == INVALID_DISPLAY) displayId = DEFAULT_DISPLAY; - final boolean isLockscreenShown = mAtmService.mTaskSupervisor.getKeyguardController() - .isKeyguardOrAodShowing(displayId); - if (!mTaskSupervisor.getLaunchParamsController() - .layoutTask(task, info.windowLayout, activity, source, options) - && !getRequestedOverrideBounds().isEmpty() - && task.isResizeable() && !isLockscreenShown) { - task.setBounds(getRequestedOverrideBounds()); + if (com.android.window.flags.Flags.fixLayoutExistingTask()) { + mTaskSupervisor.getLaunchParamsController() + .layoutTask(task, info.windowLayout, activity, source, options); + } else { + int displayId = getDisplayId(); + if (displayId == INVALID_DISPLAY) displayId = DEFAULT_DISPLAY; + final boolean isLockscreenShown = + mAtmService.mKeyguardController.isKeyguardOrAodShowing(displayId); + if (!mTaskSupervisor.getLaunchParamsController() + .layoutTask(task, info.windowLayout, activity, source, options) + && !getRequestedOverrideBounds().isEmpty() + && task.isResizeable() && !isLockscreenShown) { + task.setBounds(getRequestedOverrideBounds()); + } } - return task; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index b4c2c0173767..a11f4b1f3fc3 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -52,6 +52,7 @@ import static android.window.WindowContainerTransaction.Change.CHANGE_FOCUSABLE; import static android.window.WindowContainerTransaction.Change.CHANGE_FORCE_TRANSLUCENT; import static android.window.WindowContainerTransaction.Change.CHANGE_HIDDEN; import static android.window.WindowContainerTransaction.Change.CHANGE_RELATIVE_BOUNDS; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER; @@ -77,6 +78,8 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_SHORTCUT; +import static android.window.WindowContainerTransaction.HierarchyOp.REACHABILITY_EVENT_X; +import static android.window.WindowContainerTransaction.HierarchyOp.REACHABILITY_EVENT_Y; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_ORGANIZER; import static com.android.server.wm.ActivityRecord.State.PAUSING; @@ -1196,6 +1199,30 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub caller.mPid, caller.mUid, taskId, safeOptions)); break; } + case HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY: { + int doubleTapX = hop.getAppCompatOptions().getInt(REACHABILITY_EVENT_X); + int doubleTapY = hop.getAppCompatOptions().getInt(REACHABILITY_EVENT_Y); + final WindowContainer<?> wc = WindowContainer.fromBinder(hop.getContainer()); + if (wc == null) { + break; + } + final Task currentTask = wc.asTask(); + if (chain.mTransition != null) { + chain.mTransition.collect(wc); + } + if (currentTask != null) { + final ActivityRecord top = currentTask.topRunningActivity(); + if (top != null) { + if (chain.mTransition != null) { + chain.mTransition.collect(top); + } + top.mAppCompatController.getReachabilityPolicy().handleDoubleTap(doubleTapX, + doubleTapY); + } + } + effects |= TRANSACT_EFFECTS_CLIENT_CONFIG; + break; + } case HIERARCHY_OP_TYPE_REORDER: case HIERARCHY_OP_TYPE_REPARENT: { final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer()); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index d08715586580..54d1b7c030f4 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -211,7 +211,6 @@ public class InputMethodServiceTest { * lose flags like HIDE_IMPLICIT_ONLY. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowHideSelf() throws Exception { setShowImeWithHardKeyboard(true /* enabled */); @@ -223,13 +222,16 @@ public class InputMethodServiceTest { true /* inputViewStarted */); assertThat(mInputMethodService.isInputViewShown()).isTrue(); - // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown). - Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY), - false /* expected */, - true /* inputViewStarted */); - assertThat(mInputMethodService.isInputViewShown()).isTrue(); + if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown). + Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + false /* expected */, + true /* inputViewStarted */); + assertThat(mInputMethodService.isInputViewShown()).isTrue(); + } // IME request to hide itself without any flags, expect hidden. Log.i(TAG, "Call IMS#requestHideSelf(0)"); @@ -237,23 +239,32 @@ public class InputMethodServiceTest { () -> mInputMethodService.requestHideSelf(0 /* flags */), true /* expected */, false /* inputViewStarted */); - assertThat(mInputMethodService.isInputViewShown()).isFalse(); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the animation. Therefore, we have to + // wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } - // IME request to show itself with flag SHOW_IMPLICIT, expect shown. - Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), - true /* expected */, - true /* inputViewStarted */); - assertThat(mInputMethodService.isInputViewShown()).isTrue(); + if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // IME request to show itself with flag SHOW_IMPLICIT, expect shown. + Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)"); + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), + true /* expected */, + true /* inputViewStarted */); + assertThat(mInputMethodService.isInputViewShown()).isTrue(); - // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden. - Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY), - true /* expected */, - false /* inputViewStarted */); - assertThat(mInputMethodService.isInputViewShown()).isFalse(); + // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden. + Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + true /* expected */, + false /* inputViewStarted */); + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } } /** @@ -992,35 +1003,26 @@ public class InputMethodServiceTest { * @param expected whether the runnable is expected to trigger the signal. * @param orientationPortrait whether the orientation is expected to be portrait. */ - private void verifyFullscreenMode( - Runnable runnable, boolean expected, boolean orientationPortrait) - throws InterruptedException { - CountDownLatch signal = new CountDownLatch(1); - mInputMethodService.setCountDownLatchForTesting(signal); - - // Runnable to trigger onConfigurationChanged() - try { - runnable.run(); - } catch (Exception e) { - throw new RuntimeException(e); - } - // Waits for onConfigurationChanged() to finish. - mInstrumentation.waitForIdleSync(); - boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); - if (expected && !completed) { - fail("Timed out waiting for onConfigurationChanged()"); - } else if (!expected && completed) { - fail("Unexpected call onConfigurationChanged()"); + private void verifyFullscreenMode(@NonNull Runnable runnable, boolean expected, + boolean orientationPortrait) throws InterruptedException { + verifyInputViewStatus(runnable, expected, false /* inputViewStarted */); + if (expected) { + // Wait for the TestActivity to be recreated. + eventually(() -> + assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity)); + // Get the new TestActivity. + mActivity = TestActivity.getLastCreatedInstance(); } - clickOnEditorText(); - eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue()); + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithWindowInsetsController(), + true /* expected */, + true /* inputViewStarted */); + assertThat(mInputMethodService.isInputViewShown()).isTrue(); assertThat(mInputMethodService.getResources().getConfiguration().orientation) - .isEqualTo( - orientationPortrait - ? Configuration.ORIENTATION_PORTRAIT - : Configuration.ORIENTATION_LANDSCAPE); + .isEqualTo(orientationPortrait + ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE); EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); assertThat(editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN).isEqualTo(0); assertThat(editorInfo.internalImeOptions & EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT) @@ -1029,7 +1031,19 @@ public class InputMethodServiceTest { assertThat(mInputMethodService.onEvaluateFullscreenMode()).isEqualTo(!orientationPortrait); assertThat(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait); - mUiDevice.pressBack(); + // Hide IME before finishing the run. + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithWindowInsetsController(), + true /* expected */, + false /* inputViewStarted */); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the animation. Therefore, we have to + // wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } } private void prepareIme() throws Exception { diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index b4e885fe5661..f79cb1105611 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -61,9 +61,12 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; +import android.telecom.TelecomManager; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -166,6 +169,7 @@ public final class UserManagerServiceTest { private @Mock PackageManagerInternal mPackageManagerInternal; private @Mock KeyguardManager mKeyguardManager; private @Mock PowerManager mPowerManager; + private @Mock TelecomManager mTelecomManager; /** * Reference to the {@link UserManagerService} being tested. @@ -192,6 +196,7 @@ public final class UserManagerServiceTest { when(mSpiedContext.getSystemService(StorageManager.class)).thenReturn(mStorageManager); doReturn(mKeyguardManager).when(mSpiedContext).getSystemService(KeyguardManager.class); when(mSpiedContext.getSystemService(PowerManager.class)).thenReturn(mPowerManager); + when(mSpiedContext.getSystemService(TelecomManager.class)).thenReturn(mTelecomManager); mockGetLocalService(LockSettingsInternal.class, mLockSettingsInternal); mockGetLocalService(PackageManagerInternal.class, mPackageManagerInternal); doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any()); @@ -885,9 +890,7 @@ public final class UserManagerServiceTest { .getInteger(com.android.internal.R.integer.config_hsumBootStrategy); // Even if the headless system user switchable flag is true, the boot user should be the // first switchable full user. - doReturn(true) - .when(mSpyResources) - .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser); + mockCanSwitchToHeadlessSystemUser(true); assertThat(mUms.getBootUser()).isEqualTo(USER_ID); } @@ -906,6 +909,75 @@ public final class UserManagerServiceTest { () -> mUms.getBootUser()); } + @Test + @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + public void testGetUserLogoutability_HsumAndInteractiveHeadlessSystem_UserCanLogout() + throws Exception { + setSystemUserHeadless(true); + addUser(USER_ID); + setLastForegroundTime(USER_ID, 1_000_000L); + mockCurrentUser(USER_ID); + + mockCanSwitchToHeadlessSystemUser(true); + mockUserIsInCall(false); + + assertThat(mUms.getUserLogoutability(USER_ID)) + .isEqualTo(UserManager.LOGOUTABILITY_STATUS_OK); + } + + @Test + @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + public void testGetUserLogoutability_HsumAndNonInteractiveHeadlessSystem_UserCannotLogout() + throws Exception { + setSystemUserHeadless(true); + mockCanSwitchToHeadlessSystemUser(false); + addUser(USER_ID); + setLastForegroundTime(USER_ID, 1_000_000L); + mockCurrentUser(USER_ID); + mockUserIsInCall(false); + + assertThat(mUms.getUserLogoutability(USER_ID)) + .isEqualTo(UserManager.LOGOUTABILITY_STATUS_NO_SUITABLE_USER_TO_LOGOUT_TO); + } + + @Test + @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + public void testGetUserLogoutability_Hsum_SystemUserCannotLogout() throws Exception { + setSystemUserHeadless(true); + mockCurrentUser(UserHandle.USER_SYSTEM); + assertThat(mUms.getUserLogoutability(UserHandle.USER_SYSTEM)) + .isEqualTo(UserManager.LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER); + } + + @Test + @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + public void testGetUserLogoutability_NonHsum_SystemUserCannotLogout() throws Exception { + setSystemUserHeadless(false); + mockCurrentUser(UserHandle.USER_SYSTEM); + assertThat( + mUms.getUserLogoutability(UserHandle.USER_SYSTEM)).isEqualTo( + UserManager.LOGOUTABILITY_STATUS_CANNOT_LOGOUT_SYSTEM_USER); + } + + @Test + @EnableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + public void testGetUserLogoutability_CannotSwitch_CannotLogout() throws Exception { + setSystemUserHeadless(true); + addUser(USER_ID); + addUser(OTHER_USER_ID); + setLastForegroundTime(OTHER_USER_ID, 1_000_000L); + mockCurrentUser(USER_ID); + mUms.setUserRestriction(DISALLOW_USER_SWITCH, true, USER_ID); + assertThat(mUms.getUserLogoutability(USER_ID)) + .isEqualTo(UserManager.LOGOUTABILITY_STATUS_CANNOT_SWITCH); + } + + @Test + @DisableFlags(android.multiuser.Flags.FLAG_LOGOUT_USER_API) + public void testGetUserLogoutability_LogoutDisabled() throws Exception { + assertThrows(UnsupportedOperationException.class, () -> mUms.getUserLogoutability(USER_ID)); + } + /** * Returns true if the user's XML file has Default restrictions * @param userId Id of the user. @@ -1021,6 +1093,16 @@ public final class UserManagerServiceTest { doReturn(service).when(() -> LocalServices.getService(serviceClass)); } + private void mockCanSwitchToHeadlessSystemUser(boolean canSwitch) { + doReturn(canSwitch) + .when(mSpyResources) + .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser); + } + + private void mockUserIsInCall(boolean isInCall) { + when(mTelecomManager.isInCall()).thenReturn(isInCall); + } + private void addDefaultProfileAndParent() { addUser(PARENT_USER_ID); addProfile(PROFILE_USER_ID, PARENT_USER_ID); @@ -1063,6 +1145,7 @@ public final class UserManagerServiceTest { private void addUserData(TestUserData userData) { Log.d(TAG, "Adding " + userData); mUsers.put(userData.info.id, userData); + mUms.putUserInfo(userData.info); } private void setSystemUserHeadless(boolean headless) { diff --git a/services/tests/powerstatstests/res/raw/history_01 b/services/tests/powerstatstests/res/raw/history_01 Binary files differnew file mode 100644 index 000000000000..f69eb275f2c6 --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_01 diff --git a/services/tests/powerstatstests/res/raw/history_02 b/services/tests/powerstatstests/res/raw/history_02 Binary files differnew file mode 100644 index 000000000000..1a536ab920db --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_02 diff --git a/services/tests/powerstatstests/res/raw/history_03 b/services/tests/powerstatstests/res/raw/history_03 Binary files differnew file mode 100644 index 000000000000..76a3c7b69f01 --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_03 diff --git a/services/tests/powerstatstests/res/raw/history_04 b/services/tests/powerstatstests/res/raw/history_04 Binary files differnew file mode 100644 index 000000000000..7e43ac6281cc --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_04 diff --git a/services/tests/powerstatstests/res/raw/history_05 b/services/tests/powerstatstests/res/raw/history_05 Binary files differnew file mode 100644 index 000000000000..b587723b7d1b --- /dev/null +++ b/services/tests/powerstatstests/res/raw/history_05 diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java new file mode 100644 index 000000000000..48e0daa9dba0 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryCompressionPerfTest.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.power.stats; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.platform.test.annotations.LargeTest; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import libcore.io.Streams; + +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipParameters; +import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorInputStream; +import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorOutputStream; +import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream; +import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorOutputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; +import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.zip.Deflater; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +@RunWith(AndroidJUnit4.class) +@LargeTest +@android.platform.test.annotations.DisabledOnRavenwood(reason = "Performance test") +@Ignore("Performance experiment. Comment out @Ignore to run") +public class BatteryStatsHistoryCompressionPerfTest { + + @Rule + public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + @Rule + public final TestName mTestName = new TestName(); + + private final List<byte[]> mHistorySamples = new ArrayList<>(); + + @Before + public void loadHistorySamples() throws IOException { + Context context = InstrumentationRegistry.getContext(); + Resources resources = context.getResources(); + + for (String sampleResource + : List.of("history_01", "history_02", "history_03", "history_04", "history_05")) { + int resId = resources.getIdentifier(sampleResource, "raw", context.getPackageName()); + try (InputStream stream = resources.openRawResource(resId)) { + byte[] data = Streams.readFully(stream); + mHistorySamples.add(data); + } + } + } + + private interface StreamWrapper<T> { + T wrap(T stream) throws IOException; + } + + private static class CompressorTester implements BatteryHistoryDirectory.Compressor { + private final StreamWrapper<OutputStream> mCompressorSupplier; + private final StreamWrapper<InputStream> mUncompressorSupplier; + private final ByteArrayOutputStream mOutputStream = new ByteArrayOutputStream(200000); + private final Random mRandom = new Random(); + + private static class Sample { + public byte[] uncompressed; + public byte[] compressed; + } + + private final List<Sample> mSamples; + + CompressorTester(StreamWrapper<OutputStream> compressorSupplier, + StreamWrapper<InputStream> uncompressorSupplier, + List<byte[]> uncompressedSamples) throws IOException { + mCompressorSupplier = compressorSupplier; + mUncompressorSupplier = uncompressorSupplier; + mSamples = new ArrayList<>(); + for (byte[] uncompressed : uncompressedSamples) { + Sample s = new Sample(); + s.uncompressed = Arrays.copyOf(uncompressed, uncompressed.length); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + compress(baos, s.uncompressed); + s.compressed = baos.toByteArray(); + mSamples.add(s); + } + } + + float getCompressionRatio() { + long totalUncompressed = 0; + long totalCompressed = 0; + for (Sample sample : mSamples) { + totalUncompressed += sample.uncompressed.length; + totalCompressed += sample.compressed.length; + } + return (float) totalUncompressed / totalCompressed; + } + + void compressSample() throws IOException { + Sample sample = mSamples.get(mRandom.nextInt(mSamples.size())); + mOutputStream.reset(); + compress(mOutputStream, sample.uncompressed); + // Absence of an exception indicates success + } + + void uncompressSample() throws IOException { + Sample sample = mSamples.get(mRandom.nextInt(mSamples.size())); + uncompress(sample.uncompressed, new ByteArrayInputStream(sample.compressed)); + // Absence of an exception indicates success + } + + @Override + public void compress(OutputStream stream, byte[] data) throws IOException { + OutputStream cos = mCompressorSupplier.wrap(stream); + cos.write(data); + cos.close(); + } + + @Override + public void uncompress(byte[] data, InputStream stream) throws IOException { + InputStream cos = mUncompressorSupplier.wrap(stream); + readFully(data, cos); + } + } + + private void benchmarkCompress(StreamWrapper<OutputStream> compressorSupplier) + throws IOException { + CompressorTester tester = new CompressorTester(compressorSupplier, null, mHistorySamples); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + tester.compressSample(); + } + Bundle status = new Bundle(); + status.putFloat(mTestName.getMethodName() + "_compressionRatio", + tester.getCompressionRatio()); + InstrumentationRegistry.getInstrumentation().sendStatus(Activity.RESULT_OK, status); + } + + private void benchmarkUncompress(StreamWrapper<OutputStream> compressorSupplier, + StreamWrapper<InputStream> uncompressorSupplier) throws IOException { + CompressorTester tester = new CompressorTester(compressorSupplier, uncompressorSupplier, + mHistorySamples); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + tester.uncompressSample(); + } + } + + @Test + public void block_lz4_compress() throws IOException { + benchmarkCompress(BlockLZ4CompressorOutputStream::new); + } + + @Test + public void block_lz4_uncompress() throws IOException { + benchmarkUncompress(BlockLZ4CompressorOutputStream::new, + BlockLZ4CompressorInputStream::new); + } + + @Test + public void framed_lz4_compress() throws IOException { + benchmarkCompress(FramedLZ4CompressorOutputStream::new); + } + + @Test + public void framed_lz4_uncompress() throws IOException { + benchmarkUncompress(FramedLZ4CompressorOutputStream::new, + FramedLZ4CompressorInputStream::new); + } + + @Test + public void gzip_compress() throws IOException { + benchmarkCompress(GzipCompressorOutputStream::new); + } + + @Test + public void gzip_uncompress() throws IOException { + benchmarkUncompress(GzipCompressorOutputStream::new, + GzipCompressorInputStream::new); + } + + @Test + public void best_speed_gzip_compress() throws IOException { + benchmarkCompress(stream -> { + GzipParameters parameters = new GzipParameters(); + parameters.setCompressionLevel(Deflater.BEST_SPEED); + return new GzipCompressorOutputStream(stream, parameters); + }); + } + + @Test + public void best_speed_gzip_uncompress() throws IOException { + benchmarkUncompress(stream -> { + GzipParameters parameters = new GzipParameters(); + parameters.setCompressionLevel(Deflater.BEST_SPEED); + return new GzipCompressorOutputStream(stream, parameters); + }, GzipCompressorInputStream::new); + } + + @Test + public void java_util_gzip_compress() throws IOException { + benchmarkCompress(GZIPOutputStream::new); + } + + @Test + public void java_util_gzip_uncompress() throws IOException { + benchmarkUncompress(GZIPOutputStream::new, + GZIPInputStream::new); + } + + @Test + public void bzip2_compress() throws IOException { + benchmarkCompress(BZip2CompressorOutputStream::new); + } + + @Test + public void bzip2_uncompress() throws IOException { + benchmarkUncompress(BZip2CompressorOutputStream::new, + BZip2CompressorInputStream::new); + } + + @Test + public void xz_compress() throws IOException { + benchmarkCompress(XZCompressorOutputStream::new); + } + + @Test + public void xz_uncompress() throws IOException { + benchmarkUncompress(XZCompressorOutputStream::new, + XZCompressorInputStream::new); + } + + @Test + public void deflate_compress() throws IOException { + benchmarkCompress(DeflateCompressorOutputStream::new); + } + + @Test + public void deflate_uncompress() throws IOException { + benchmarkUncompress(DeflateCompressorOutputStream::new, + DeflateCompressorInputStream::new); + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index 164eec6fbc49..8fad93184732 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -17,6 +17,7 @@ package com.android.server.power.stats; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -30,18 +31,20 @@ import android.os.BatteryConsumer; import android.os.BatteryManager; import android.os.BatteryStats; import android.os.BatteryStats.HistoryItem; +import android.os.ConditionVariable; import android.os.Parcel; import android.os.PersistableBundle; import android.os.Process; import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; +import android.platform.test.ravenwood.RavenwoodRule; import android.telephony.NetworkRegistrationInfo; -import android.util.AtomicFile; import android.util.Log; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.os.BackgroundThread; import com.android.internal.os.BatteryStatsHistory; import com.android.internal.os.BatteryStatsHistoryIterator; import com.android.internal.os.MonotonicClock; @@ -58,6 +61,8 @@ import org.mockito.MockitoAnnotations; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.file.Files; @@ -85,6 +90,7 @@ public class BatteryStatsHistoryTest { private File mHistoryDir; private final MockClock mClock = new MockClock(); private final MonotonicClock mMonotonicClock = new MonotonicClock(0, mClock); + private BatteryHistoryDirectory mDirectory; private BatteryStatsHistory mHistory; private BatteryStats.HistoryPrinter mHistoryPrinter; @Mock @@ -108,11 +114,30 @@ public class BatteryStatsHistoryTest { } mHistoryDir.delete(); + + BatteryHistoryDirectory.Compressor compressor; + if (RavenwoodRule.isOnRavenwood()) { + compressor = new BatteryHistoryDirectory.Compressor() { + @Override + public void compress(OutputStream stream, byte[] data) throws IOException { + stream.write(data); + } + + @Override + public void uncompress(byte[] data, InputStream stream) throws IOException { + readFully(data, stream); + } + }; + } else { + compressor = BatteryHistoryDirectory.DEFAULT_COMPRESSOR; + } + mDirectory = new BatteryHistoryDirectory(mHistoryDir, 32768, compressor); + mClock.realtime = 123; mClock.currentTime = 1743645660000L; // 2025-04-03, 2:01:00 AM - mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32768, - MAX_HISTORY_BUFFER_SIZE, mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, + mHistory = new BatteryStatsHistory(mHistoryBuffer, MAX_HISTORY_BUFFER_SIZE, mDirectory, + mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger); mHistory.forceRecordAllHistory(); mHistory.startRecordingHistory(mClock.realtime, mClock.uptime, false); @@ -210,8 +235,9 @@ public class BatteryStatsHistoryTest { } @Test - public void testStartNextFile() throws Exception { + public void testStartNextFile() { mHistory.forceRecordAllHistory(); + mDirectory.setFileCompressionEnabled(false); mClock.realtime = 123; @@ -225,7 +251,7 @@ public class BatteryStatsHistoryTest { mClock.realtime = 1000 * i; fileList.add(mClock.realtime + ".bh"); - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); createActiveFile(mHistory); fillActiveFile(mHistory); @@ -235,8 +261,9 @@ public class BatteryStatsHistoryTest { // create file 32 mClock.realtime = 1000 * 32; - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); createActiveFile(mHistory); + fillActiveFile(mHistory); fileList.add("32000.bh"); fileList.remove(0); // verify file 0 is deleted. @@ -244,21 +271,22 @@ public class BatteryStatsHistoryTest { verifyFileNames(mHistory, fileList); verifyActiveFile(mHistory, "32000.bh"); - fillActiveFile(mHistory); - // create file 33 mClock.realtime = 1000 * 33; - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); createActiveFile(mHistory); - // verify file 1 is deleted + fillActiveFile(mHistory); fileList.add("33000.bh"); fileList.remove(0); + mHistory.writeHistory(); + + // verify file 1 is deleted verifyFileDeleted("1000.bh"); verifyFileNames(mHistory, fileList); verifyActiveFile(mHistory, "33000.bh"); // create a new BatteryStatsHistory object, it will pick up existing history files. - BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024, + BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, 1024, mDirectory, null, mClock, mMonotonicClock, mTracer, mEventLogger); // verify constructor can pick up all files from file system. verifyFileNames(history2, fileList); @@ -281,7 +309,7 @@ public class BatteryStatsHistoryTest { // create file 1. mClock.realtime = 2345678; - history2.startNextFile(mClock.realtime); + history2.startNextFragment(mClock.realtime); createActiveFile(history2); verifyFileNames(history2, Arrays.asList("1234567.bh", "2345678.bh")); verifyActiveFile(history2, "2345678.bh"); @@ -297,10 +325,10 @@ public class BatteryStatsHistoryTest { mHistory = spy(mHistory.copy()); doAnswer(invocation -> { - AtomicFile file = invocation.getArgument(1); - mReadFiles.add(file.getBaseFile().getName()); + BatteryHistoryDirectory.BatteryHistoryFile file = invocation.getArgument(1); + mReadFiles.add(file.atomicFile.getBaseFile().getName()); return invocation.callRealMethod(); - }).when(mHistory).readFileToParcel(any(), any()); + }).when(mHistory).readFragmentToParcel(any(), any()); // Prepare history for iteration mHistory.iterate(0, MonotonicClock.UNDEFINED); @@ -339,10 +367,10 @@ public class BatteryStatsHistoryTest { mHistory = spy(mHistory.copy()); doAnswer(invocation -> { - AtomicFile file = invocation.getArgument(1); - mReadFiles.add(file.getBaseFile().getName()); + BatteryHistoryDirectory.BatteryHistoryFile file = invocation.getArgument(1); + mReadFiles.add(file.atomicFile.getBaseFile().getName()); return invocation.callRealMethod(); - }).when(mHistory).readFileToParcel(any(), any()); + }).when(mHistory).readFragmentToParcel(any(), any()); // Prepare history for iteration mHistory.iterate(1000, 3000); @@ -371,14 +399,14 @@ public class BatteryStatsHistoryTest { mHistory.recordEvent(mClock.realtime, mClock.uptime, BatteryStats.HistoryItem.EVENT_JOB_START, "job", 42); - mHistory.startNextFile(mClock.realtime); // 1000.bh + mHistory.startNextFragment(mClock.realtime); // 1000.bh mClock.realtime = 2000; mClock.uptime = 2000; mHistory.recordEvent(mClock.realtime, mClock.uptime, BatteryStats.HistoryItem.EVENT_JOB_FINISH, "job", 42); - mHistory.startNextFile(mClock.realtime); // 2000.bh + mHistory.startNextFragment(mClock.realtime); // 2000.bh mClock.realtime = 3000; mClock.uptime = 3000; @@ -386,30 +414,37 @@ public class BatteryStatsHistoryTest { HistoryItem.EVENT_ALARM, "alarm", 42); // Flush accumulated history to disk - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); } private void verifyActiveFile(BatteryStatsHistory history, String file) { final File expectedFile = new File(mHistoryDir, file); - assertEquals(expectedFile.getPath(), history.getActiveFile().getBaseFile().getPath()); + assertEquals(expectedFile.getPath(), + ((BatteryHistoryDirectory.BatteryHistoryFile) history.getActiveFragment()) + .atomicFile.getBaseFile().getPath()); assertTrue(expectedFile.exists()); } private void verifyFileNames(BatteryStatsHistory history, List<String> fileList) { - assertEquals(fileList.size(), history.getFilesNames().size()); + awaitCompletion(); + List<String> fileNames = + ((BatteryHistoryDirectory) history.getBatteryHistoryStore()).getFileNames(); + assertThat(fileNames).isEqualTo(fileList); for (int i = 0; i < fileList.size(); i++) { - assertEquals(fileList.get(i), history.getFilesNames().get(i)); final File expectedFile = new File(mHistoryDir, fileList.get(i)); - assertTrue(expectedFile.exists()); + assertWithMessage("File does not exist " + expectedFile) + .that(expectedFile.exists()).isTrue(); } } private void verifyFileDeleted(String file) { + awaitCompletion(); assertFalse(new File(mHistoryDir, file).exists()); } private void createActiveFile(BatteryStatsHistory history) { - final File file = history.getActiveFile().getBaseFile(); + File file = ((BatteryHistoryDirectory.BatteryHistoryFile) history.getActiveFragment()) + .atomicFile.getBaseFile(); if (file.exists()) { return; } @@ -561,7 +596,7 @@ public class BatteryStatsHistoryTest { public void largeTagPool() { // Keep the preserved part of history short - we only need to capture the very tail of // history. - mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 1, 6000, + mHistory = new BatteryStatsHistory(mHistoryBuffer, 6000, mDirectory, mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger); mHistory.forceRecordAllHistory(); @@ -699,7 +734,7 @@ public class BatteryStatsHistoryTest { assertThat(size).isGreaterThan(lastHistorySize); lastHistorySize = size; - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); size = mHistory.getMonotonicHistorySize(); assertThat(size).isEqualTo(lastHistorySize); @@ -713,7 +748,7 @@ public class BatteryStatsHistoryTest { assertThat(size).isGreaterThan(lastHistorySize); lastHistorySize = size; - mHistory.startNextFile(mClock.realtime); + mHistory.startNextFragment(mClock.realtime); mClock.realtime = 3000; mClock.uptime = 3000; @@ -788,4 +823,58 @@ public class BatteryStatsHistoryTest { parcel.recycle(); } + + @Test + public void compressHistoryFiles() { + // The first history file will be uncompressed + mDirectory.setFileCompressionEnabled(false); + + mClock.realtime = 1000; + mClock.uptime = 1000; + mHistory.recordEvent(mClock.realtime, mClock.uptime, + BatteryStats.HistoryItem.EVENT_JOB_START, "job", 42); + + mHistory.startNextFragment(mClock.realtime); + + // The second file will be compressed + mDirectory.setFileCompressionEnabled(true); + + mClock.realtime = 2000; + mClock.uptime = 2000; + mHistory.recordEvent(mClock.realtime, mClock.uptime, + BatteryStats.HistoryItem.EVENT_JOB_FINISH, "job", 42); + + mHistory.startNextFragment(mClock.realtime); + + awaitCompletion(); + + assertThat(historySummary(mHistory)).isEqualTo(List.of("+42:job", "-42:job")); + + Parcel parcel = Parcel.obtain(); + mHistory.writeToBatteryUsageStatsParcel(parcel, Long.MAX_VALUE); + parcel.setDataPosition(0); + + BatteryStatsHistory actual = BatteryStatsHistory.createFromBatteryUsageStatsParcel(parcel); + assertThat(historySummary(actual)).isEqualTo(List.of("+42:job", "-42:job")); + } + + private List<String> historySummary(BatteryStatsHistory history) { + List<String> events = new ArrayList<>(); + try (BatteryStatsHistoryIterator it = history.iterate(0, Long.MAX_VALUE)) { + HistoryItem item; + while ((item = it.next()) != null) { + if ((item.eventCode & HistoryItem.EVENT_TYPE_MASK) == HistoryItem.EVENT_JOB) { + events.add(((item.eventCode & HistoryItem.EVENT_FLAG_START) != 0 ? "+" : "-") + + item.eventTag.uid + ":" + item.eventTag.string); + } + } + } + return events; + } + + private static void awaitCompletion() { + ConditionVariable done = new ConditionVariable(); + BackgroundThread.getHandler().post(done::open); + done.block(); + } } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java index e94ef5bb4871..31ff50f8ca58 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java @@ -877,9 +877,19 @@ public class BatteryUsageStatsProviderTest { } @Test - @EnableFlags(Flags.FLAG_EXTENDED_BATTERY_HISTORY_CONTINUOUS_COLLECTION_ENABLED) + @EnableFlags({ + Flags.FLAG_EXTENDED_BATTERY_HISTORY_CONTINUOUS_COLLECTION_ENABLED, + Flags.FLAG_EXTENDED_BATTERY_HISTORY_COMPRESSION_ENABLED + }) public void testIncludeSubsetOfHistory() throws IOException { MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); + BatteryHistoryDirectory store = + (BatteryHistoryDirectory) batteryStats.getHistory().getBatteryHistoryStore(); + store.setFileCompressionEnabled(true); + // Make history fragment size predictable. Without this protection, holding the history + // directory lock in the background would prevent new fragments from being created. + store.makeDirectoryLockUnconditional(); + batteryStats.getHistory().setMaxHistoryBufferSize(100); synchronized (batteryStats) { batteryStats.setRecordAllHistoryLocked(true); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java index a69e2fdb0b03..c7a19ce7b233 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java @@ -41,6 +41,9 @@ import com.android.internal.os.PowerProfile; import com.android.internal.power.EnergyConsumerStats; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Queue; @@ -49,6 +52,18 @@ import java.util.Queue; * Mocks a BatteryStatsImpl object. */ public class MockBatteryStatsImpl extends BatteryStatsImpl { + public static final BatteryHistoryDirectory.Compressor PASS_THROUGH_COMPRESSOR = + new BatteryHistoryDirectory.Compressor() { + @Override + public void compress(OutputStream stream, byte[] data) throws IOException { + stream.write(data); + } + + @Override + public void uncompress(byte[] data, InputStream stream) throws IOException { + readFully(data, stream); + } + }; public boolean mForceOnBattery; // The mNetworkStats will be used for both wifi and mobile categories private NetworkStats mNetworkStats; @@ -83,7 +98,11 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { MockBatteryStatsImpl(BatteryStatsConfig config, Clock clock, MonotonicClock monotonicClock, File historyDirectory, Handler handler, PowerProfile powerProfile, PowerStatsUidResolver powerStatsUidResolver) { - super(config, clock, monotonicClock, historyDirectory, handler, + super(config, clock, monotonicClock, historyDirectory, + historyDirectory != null ? new BatteryHistoryDirectory( + new File(historyDirectory, "battery-history"), + config.getMaxHistorySizeBytes(), PASS_THROUGH_COMPRESSOR) : null, + handler, mock(PlatformIdleStateCallback.class), mock(EnergyStatsRetriever.class), mock(UserInfoProvider.class), powerProfile, new CpuScalingPolicies(new SparseArray<>(), new SparseArray<>()), diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java index 3bdbcb50e601..73d491c93bb5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsAggregatorTest.java @@ -59,7 +59,7 @@ public class PowerStatsAggregatorTest { @Before public void setup() throws ParseException { - mHistory = new BatteryStatsHistory(null, null, 0, 1024, + mHistory = new BatteryStatsHistory(null, 1024, null, mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock, mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class), null); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java index d243f92a139f..9ef58cc28a69 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PowerStatsExporterTest.java @@ -42,6 +42,7 @@ import com.android.internal.os.CpuScalingPolicies; import com.android.internal.os.MonotonicClock; import com.android.internal.os.PowerProfile; import com.android.internal.os.PowerStats; +import com.android.server.power.stats.BatteryHistoryDirectory; import com.android.server.power.stats.BatteryUsageStatsRule; import com.android.server.power.stats.MockClock; import com.android.server.power.stats.PowerStatsStore; @@ -84,6 +85,7 @@ public class PowerStatsExporterTest { private PowerStatsStore mPowerStatsStore; private PowerStatsAggregator mPowerStatsAggregator; private MultiStatePowerAttributor mPowerAttributor; + private BatteryHistoryDirectory mDirectory; private BatteryStatsHistory mHistory; private CpuPowerStatsLayout mCpuStatsArrayLayout; private PowerStats.Descriptor mPowerStatsDescriptor; @@ -117,7 +119,8 @@ public class PowerStatsExporterTest { AggregatedPowerStatsConfig.STATE_PROCESS_STATE); mPowerStatsStore = new PowerStatsStore(storeDirectory, new TestHandler()); - mHistory = new BatteryStatsHistory(Parcel.obtain(), storeDirectory, 0, 10000, + mDirectory = new BatteryHistoryDirectory(storeDirectory, 0); + mHistory = new BatteryStatsHistory(Parcel.obtain(), 10000, mDirectory, mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock, mMonotonicClock, null, null); mPowerStatsAggregator = new PowerStatsAggregator(config); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java index ed3cda0f76ef..8257d714a5d5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WakelockPowerStatsProcessorTest.java @@ -87,7 +87,7 @@ public class WakelockPowerStatsProcessorTest { PowerStats ps = new PowerStats(descriptor); long[] uidStats = new long[descriptor.uidStatsArrayLength]; - BatteryStatsHistory history = new BatteryStatsHistory(null, null, 0, 10000, + BatteryStatsHistory history = new BatteryStatsHistory(null, 10000, null, mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mStatsRule.getMockClock(), new MonotonicClock(START_TIME, mStatsRule.getMockClock()), null, null); diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java new file mode 100644 index 000000000000..2e489a81c43a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.location.contexthub; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.hardware.contexthub.HubEndpointInfo; +import android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier; +import android.hardware.contexthub.IContextHubEndpoint; +import android.hardware.contexthub.IContextHubEndpointCallback; +import android.hardware.contexthub.IEndpointCommunication; +import android.os.Binder; +import android.os.RemoteException; +import android.platform.test.annotations.Postsubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Collections; + +@RunWith(AndroidJUnit4.class) +@Postsubmit +// TODO(b/378944402): Enable test in presubmit +public class ContextHubEndpointTest { + private static final int SESSION_ID_RANGE = ContextHubEndpointManager.SERVICE_SESSION_RANGE; + private static final int MIN_SESSION_ID = 0; + private static final int MAX_SESSION_ID = MIN_SESSION_ID + SESSION_ID_RANGE - 1; + + private static final String ENDPOINT_NAME = "Example test endpoint"; + private static final int ENDPOINT_ID = 1; + private static final String ENDPOINT_PACKAGE_NAME = "com.android.server.location.contexthub"; + + private ContextHubClientManager mClientManager; + private ContextHubEndpointManager mEndpointManager; + private HubInfoRegistry mHubInfoRegistry; + private ContextHubTransactionManager mTransactionManager; + private Context mContext; + @Mock private IEndpointCommunication mMockEndpointCommunications; + @Mock private IContextHubWrapper mMockContextHubWrapper; + @Mock private IContextHubEndpointCallback mMockCallback; + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Before + public void setUp() throws RemoteException, InstantiationException { + when(mMockContextHubWrapper.getHubs()).thenReturn(Collections.emptyList()); + when(mMockContextHubWrapper.getEndpoints()).thenReturn(Collections.emptyList()); + when(mMockContextHubWrapper.registerEndpointHub(any(), any())) + .thenReturn(mMockEndpointCommunications); + when(mMockEndpointCommunications.requestSessionIdRange(SESSION_ID_RANGE)) + .thenReturn(new int[] {MIN_SESSION_ID, MAX_SESSION_ID}); + when(mMockCallback.asBinder()).thenReturn(new Binder()); + + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + mHubInfoRegistry = new HubInfoRegistry(mContext, mMockContextHubWrapper); + mClientManager = new ContextHubClientManager(mContext, mMockContextHubWrapper); + mTransactionManager = + new ContextHubTransactionManager( + mMockContextHubWrapper, mClientManager, new NanoAppStateManager()); + mEndpointManager = + new ContextHubEndpointManager( + mContext, mMockContextHubWrapper, mHubInfoRegistry, mTransactionManager); + mEndpointManager.init(); + } + + @Test + public void testRegisterEndpoint() throws RemoteException { + // Register an endpoint and confirm we can get a valid IContextHubEndoint reference + HubEndpointInfo info = + new HubEndpointInfo( + ENDPOINT_NAME, ENDPOINT_ID, ENDPOINT_PACKAGE_NAME, Collections.emptyList()); + IContextHubEndpoint endpoint = + mEndpointManager.registerEndpoint( + info, mMockCallback, ENDPOINT_PACKAGE_NAME, /* attributionTag= */ null); + assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(1); + assertThat(endpoint).isNotNull(); + HubEndpointInfo assignedInfo = endpoint.getAssignedHubEndpointInfo(); + assertThat(assignedInfo).isNotNull(); + HubEndpointIdentifier assignedIdentifier = assignedInfo.getIdentifier(); + assertThat(assignedIdentifier).isNotNull(); + + // Unregister the endpoint and confirm proper clean-up + mEndpointManager.unregisterEndpoint(assignedIdentifier.getEndpoint()); + assertThat(mEndpointManager.getNumRegisteredClients()).isEqualTo(0); + } + + @Test + public void testReserveSessionId() { + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + + int sessionId = mEndpointManager.reserveSessionId(); + assertThat(sessionId).isAtLeast(MIN_SESSION_ID); + assertThat(sessionId).isAtMost(MAX_SESSION_ID); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE - 1); + + mEndpointManager.returnSessionId(sessionId); + assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 01d34b697def..3aa95449cc98 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -606,8 +606,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return FlagsParameterization.allCombinationsOf( - FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NM_BINDER_PERF_CACHE_CHANNELS); + return FlagsParameterization.allCombinationsOf(); } public NotificationManagerServiceTest(FlagsParameterization flags) { diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java index 5265b442c968..67a95de8a5c1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java @@ -39,6 +39,7 @@ import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier. import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import android.annotation.NonNull; import android.app.ActivityOptions; import android.content.ComponentName; import android.content.pm.ActivityInfo.WindowLayout; @@ -293,7 +294,7 @@ public class LaunchParamsControllerTests extends WindowTestsBase { final int beforeWindowMode = task.getWindowingMode(); assertNotEquals(windowingMode, beforeWindowMode); - mController.layoutTask(task, null /* windowLayout */); + layoutTask(task); final int afterWindowMode = task.getWindowingMode(); assertEquals(afterWindowMode, beforeWindowMode); @@ -317,7 +318,7 @@ public class LaunchParamsControllerTests extends WindowTestsBase { assertNotEquals(expected, task.getBounds()); - mController.layoutTask(task, null /* windowLayout */); + layoutTask(task); // Task will make adjustments to requested bounds. We only need to guarantee that the // reuqested bounds are expected. @@ -342,7 +343,7 @@ public class LaunchParamsControllerTests extends WindowTestsBase { assertNotEquals(expected, task.getBounds()); - mController.layoutTask(task, null /* windowLayout */); + layoutTask(task); assertEquals(expected, task.getRequestedOverrideBounds()); } @@ -365,7 +366,7 @@ public class LaunchParamsControllerTests extends WindowTestsBase { assertNotEquals(expected, task.getBounds()); - mController.layoutTask(task, null /* windowLayout */); + layoutTask(task); assertNotEquals(expected, task.getBounds()); assertEquals(expected, task.mLastNonFullscreenBounds); @@ -467,4 +468,9 @@ public class LaunchParamsControllerTests extends WindowTestsBase { private TestDisplayContent createNewDisplayContent() { return addNewDisplayContentAt(DisplayContent.POSITION_TOP); } + + private void layoutTask(@NonNull Task task) { + mController.layoutTask(task, null /* layout */, null /* activity */, null /* source */, + null /* options */); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java index 369600c3f8d7..dcb68620e361 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTransactionTests.java @@ -18,20 +18,30 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY; +import static android.window.WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID; +import static android.window.WindowContainerTransaction.HierarchyOp.REACHABILITY_EVENT_X; +import static android.window.WindowContainerTransaction.HierarchyOp.REACHABILITY_EVENT_Y; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.times; import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; import android.platform.test.annotations.Presubmit; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import android.window.WindowContainerTransaction.HierarchyOp; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; @@ -223,6 +233,31 @@ public class WindowContainerTransactionTests extends WindowTestsBase { < tda.mChildren.indexOf(desktopOrganizer.mTasks.get(2).getRootTask())); } + @Test + public void testAppCompat_setReachabilityOffsets() { + final Task task = createTask(/* taskId */ 37); + final WindowContainerToken containerToken = task.getTaskInfo().token; + spyOn(containerToken); + final Binder asBinder = new Binder(); + doReturn(asBinder).when(containerToken).asBinder(); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setReachabilityOffset(containerToken, /* taskId */ task.mTaskId, 10, 20); + + final List<HierarchyOp> hierarchyOps = wct.getHierarchyOps().stream() + .filter(op -> op.getType() == HIERARCHY_OP_TYPE_APP_COMPAT_REACHABILITY) + .toList(); + + assertEquals(1, hierarchyOps.size()); + final HierarchyOp appCompatOp = hierarchyOps.getFirst(); + assertNotNull(appCompatOp); + final Bundle appCompatOptions = appCompatOp.getAppCompatOptions(); + + assertEquals(task.mTaskId, appCompatOptions.getInt(LAUNCH_KEY_TASK_ID)); + assertEquals(10, appCompatOptions.getInt(REACHABILITY_EVENT_X)); + assertEquals(20, appCompatOptions.getInt(REACHABILITY_EVENT_Y)); + assertSame(asBinder, appCompatOp.getContainer()); + } + private Task createTask(int taskId) { return new Task.Builder(mAtm) .setTaskId(taskId) diff --git a/telephony/java/android/telephony/SubscriptionInfo.java b/telephony/java/android/telephony/SubscriptionInfo.java index d164c8851f5b..4b175c134d84 100644 --- a/telephony/java/android/telephony/SubscriptionInfo.java +++ b/telephony/java/android/telephony/SubscriptionInfo.java @@ -618,9 +618,9 @@ public class SubscriptionInfo implements Parcelable { @Deprecated public int getMcc() { try { - return mMcc == null ? 0 : Integer.parseInt(mMcc); + return TextUtils.isEmpty(mMcc) ? 0 : Integer.parseInt(mMcc); } catch (NumberFormatException e) { - Log.w(SubscriptionInfo.class.getSimpleName(), "MCC string is not a number"); + Log.w(SubscriptionInfo.class.getSimpleName(), "MCC string is not a number: " + mMcc); return 0; } } @@ -633,9 +633,9 @@ public class SubscriptionInfo implements Parcelable { @Deprecated public int getMnc() { try { - return mMnc == null ? 0 : Integer.parseInt(mMnc); + return TextUtils.isEmpty(mMnc) ? 0 : Integer.parseInt(mMnc); } catch (NumberFormatException e) { - Log.w(SubscriptionInfo.class.getSimpleName(), "MNC string is not a number"); + Log.w(SubscriptionInfo.class.getSimpleName(), "MNC string is not a number: " + mMnc); return 0; } } diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 73ea68bc3547..504605d0a1a2 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -19432,7 +19432,6 @@ public class TelephonyManager { * and integrity algorithms in use * @hide */ - @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY) @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @SystemApi public void setNullCipherNotificationsEnabled(boolean enable) { @@ -19459,7 +19458,6 @@ public class TelephonyManager { * and integrity algorithms in use * @hide */ - @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY) @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @SystemApi public boolean isNullCipherNotificationsEnabled() { |