summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java7
-rw-r--r--core/api/system-current.txt58
-rw-r--r--core/java/android/content/pm/PackageManager.java19
-rw-r--r--core/java/android/content/pm/flags.aconfig8
-rw-r--r--core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl34
-rw-r--r--core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl28
-rw-r--r--core/java/android/content/pm/verify/pkg/IVerifierService.aidl31
-rw-r--r--core/java/android/content/pm/verify/pkg/VerificationSession.aidl20
-rw-r--r--core/java/android/content/pm/verify/pkg/VerificationSession.java276
-rw-r--r--core/java/android/content/pm/verify/pkg/VerificationStatus.aidl20
-rw-r--r--core/java/android/content/pm/verify/pkg/VerificationStatus.java166
-rw-r--r--core/java/android/content/pm/verify/pkg/VerifierService.java113
-rw-r--r--core/java/android/service/notification/ZenModeDiff.java45
-rw-r--r--core/java/android/view/autofill/AutofillStateFingerprint.java2
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig7
-rw-r--r--core/res/AndroidManifest.xml23
-rw-r--r--core/res/res/values/config.xml5
-rw-r--r--core/res/res/values/symbols.xml2
-rw-r--r--core/tests/coretests/Android.bp2
-rw-r--r--core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java155
-rw-r--r--core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java72
-rw-r--r--core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java92
-rw-r--r--core/tests/utiltests/Android.bp2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt12
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt186
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java38
-rw-r--r--media/java/android/media/RoutingSessionInfo.java27
-rw-r--r--media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java22
-rw-r--r--packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml31
-rw-r--r--packages/SettingsLib/ButtonPreference/res/values/attrs.xml4
-rw-r--r--packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java4
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt1
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt42
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt7
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt3
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt42
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt10
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt36
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt32
-rw-r--r--packages/SystemUI/customization/Android.bp1
-rw-r--r--packages/SystemUI/customization/res/values/ids.xml9
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt51
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt448
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt21
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt288
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt30
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt2
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt104
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt64
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt32
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt117
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt180
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt260
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt654
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt55
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt76
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt72
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt72
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java75
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt8
-rw-r--r--packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json88
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto3
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java59
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java79
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java22
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt98
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt61
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt79
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java14
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt18
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt21
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java1
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java4
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java6
-rw-r--r--services/core/java/com/android/server/media/MediaSessionRecord.java50
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java9
-rw-r--r--services/core/java/com/android/server/power/hint/HintManagerService.java2
-rw-r--r--services/tests/servicestests/Android.bp14
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java230
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java15
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java5
-rw-r--r--telephony/java/android/telephony/TelephonyManager.java19
111 files changed, 4847 insertions, 740 deletions
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
index 238c028fa0cf..9eac10846319 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
@@ -18,6 +18,7 @@ package android.libcore.regression;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
+import android.util.Log;
import androidx.test.filters.LargeTest;
@@ -47,6 +48,8 @@ import javax.crypto.spec.IvParameterSpec;
@RunWith(JUnitParamsRunner.class)
@LargeTest
public class CipherPerfTest {
+ private static final String TAG = "android.libcore.regression.CipherPerfTest";
+
@Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
public static Collection getCases() {
@@ -71,6 +74,10 @@ public class CipherPerfTest {
}
for (int keySize : keySizes) {
for (int inputSize : inputSizes) {
+ Log.i(TAG,
+ "param[" + params.size() + "] = " + mode.name() + ", "
+ + padding.name() + ", " + keySize + ", " + inputSize
+ + ", " + implementation.name());
params.add(
new Object[] {
mode, padding, keySize, inputSize, implementation
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 1a1f3548fa74..4b6c62ecb032 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -91,6 +91,7 @@ package android {
field public static final String BIND_TRANSLATION_SERVICE = "android.permission.BIND_TRANSLATION_SERVICE";
field public static final String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT";
field public static final String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE";
+ field @FlaggedApi("android.content.pm.verification_service") public static final String BIND_VERIFICATION_AGENT = "android.permission.BIND_VERIFICATION_AGENT";
field public static final String BIND_VISUAL_QUERY_DETECTION_SERVICE = "android.permission.BIND_VISUAL_QUERY_DETECTION_SERVICE";
field public static final String BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE = "android.permission.BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE";
field public static final String BIND_WEARABLE_SENSING_SERVICE = "android.permission.BIND_WEARABLE_SENSING_SERVICE";
@@ -412,6 +413,7 @@ package android {
field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String USE_ON_DEVICE_INTELLIGENCE = "android.permission.USE_ON_DEVICE_INTELLIGENCE";
field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK";
field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED";
+ field @FlaggedApi("android.content.pm.verification_service") public static final String VERIFICATION_AGENT = "android.permission.VERIFICATION_AGENT";
field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String VIBRATE_VENDOR_EFFECTS = "android.permission.VIBRATE_VENDOR_EFFECTS";
field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS";
field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS";
@@ -4303,6 +4305,7 @@ package android.content.pm {
method @Deprecated @RequiresPermission(android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT) public abstract void verifyIntentFilter(int, int, @NonNull java.util.List<java.lang.String>);
field public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS";
field public static final String ACTION_REQUEST_PERMISSIONS_FOR_OTHER = "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
+ field @FlaggedApi("android.content.pm.verification_service") public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE";
field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_APK = 1; // 0x1
field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_INSTALLER = 2; // 0x2
field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_SYSTEM_IMAGE = 3; // 0x3
@@ -4616,6 +4619,61 @@ package android.content.pm.verify.domain {
}
+package android.content.pm.verify.pkg {
+
+ @FlaggedApi("android.content.pm.verification_service") public final class VerificationSession implements android.os.Parcelable {
+ method public int describeContents();
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long extendTimeRemaining(long);
+ method @NonNull public java.util.List<android.content.pm.SharedLibraryInfo> getDeclaredLibraries();
+ method @NonNull public android.os.PersistableBundle getExtensionParams();
+ method public int getId();
+ method public int getInstallSessionId();
+ method @NonNull public String getPackageName();
+ method @NonNull public android.content.pm.SigningInfo getSigningInfo();
+ method @NonNull public android.net.Uri getStagedPackageUri();
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long getTimeoutTime();
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus);
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus, @NonNull android.os.PersistableBundle);
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationIncomplete(int);
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationSession> CREATOR;
+ field public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; // 0x2
+ field public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; // 0x1
+ field public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; // 0x0
+ }
+
+ @FlaggedApi("android.content.pm.verification_service") public final class VerificationStatus implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAslStatus();
+ method @NonNull public String getFailureMessage();
+ method public boolean isVerified();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationStatus> CREATOR;
+ field public static final int VERIFIER_STATUS_ASL_BAD = 2; // 0x2
+ field public static final int VERIFIER_STATUS_ASL_GOOD = 1; // 0x1
+ field public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class VerificationStatus.Builder {
+ ctor public VerificationStatus.Builder();
+ method @NonNull public android.content.pm.verify.pkg.VerificationStatus build();
+ method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setAslStatus(int);
+ method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setFailureMessage(@NonNull String);
+ method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setVerified(boolean);
+ }
+
+ @FlaggedApi("android.content.pm.verification_service") public abstract class VerifierService extends android.app.Service {
+ ctor public VerifierService();
+ method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent);
+ method public abstract void onPackageNameAvailable(@NonNull String);
+ method public abstract void onVerificationCancelled(@NonNull String);
+ method public abstract void onVerificationRequired(@NonNull android.content.pm.verify.pkg.VerificationSession);
+ method public abstract void onVerificationRetry(@NonNull android.content.pm.verify.pkg.VerificationSession);
+ method public abstract void onVerificationTimeout(int);
+ }
+
+}
+
package android.content.rollback {
public final class PackageRollbackInfo implements android.os.Parcelable {
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index fb2655c771c4..e985f88f38fc 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -5039,6 +5039,25 @@ public abstract class PackageManager {
"android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
/**
+ * Used by the system to query a {@link android.content.pm.verify.pkg.VerifierService} provider,
+ * which registers itself via an intent-filter handling this action.
+ *
+ * <p class="note">Only the system can bind to such a verifier service. This is protected by the
+ * {@link android.Manifest.permission#BIND_VERIFICATION_AGENT} permission. The verifier service
+ * app should protect the service by adding this permission in the service declaration in its
+ * manifest.
+ * <p>
+ * A verifier service must be a privileged app and hold the
+ * {@link android.Manifest.permission#VERIFICATION_AGENT} permission.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(android.content.pm.Flags.FLAG_VERIFICATION_SERVICE)
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE";
+
+ /**
* The names of the requested permissions.
* <p>
* <strong>Type:</strong> String[]
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 160cbdffe5bb..300740e84c60 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -309,3 +309,11 @@ flag {
description: "Feature flag to enable the holder of SYSTEM_APP_PROTECTION_SERVICE role to silently delete packages. To be deprecated by delete_packages_silently."
bug: "361776825"
}
+
+flag {
+ name: "verification_service"
+ namespace: "package_manager_service"
+ description: "Feature flag to enable the new verification service."
+ bug: "360129103"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl
new file mode 100644
index 000000000000..38a7956603ae
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.os.PersistableBundle;
+
+/**
+ * Oneway interface that allows the verifier to send response or verification results back to
+ * the system.
+ * @hide
+ */
+oneway interface IVerificationSessionCallback {
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ void reportVerificationIncomplete(int verificationId, int reason);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ void reportVerificationComplete(int verificationId, in VerificationStatus status);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ void reportVerificationCompleteWithExtensionResponse(int verificationId, in VerificationStatus status, in PersistableBundle response);
+}
diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
new file mode 100644
index 000000000000..7a9484abd1b1
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+/**
+ * Non-oneway interface that allows the verifier to retrieve information from the system.
+ * @hide
+ */
+interface IVerificationSessionInterface {
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ long getTimeoutTime(int verificationId);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ long extendTimeRemaining(int verificationId, long additionalMs);
+} \ No newline at end of file
diff --git a/core/java/android/content/pm/verify/pkg/IVerifierService.aidl b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl
new file mode 100644
index 000000000000..d3071fd67b9d
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.content.pm.verify.pkg.VerificationSession;
+
+/**
+ * Oneway interface that allows the system to communicate to the verifier service agent.
+ * @hide
+ */
+oneway interface IVerifierService {
+ void onPackageNameAvailable(in String packageName);
+ void onVerificationCancelled(in String packageName);
+ void onVerificationRequired(in VerificationSession session);
+ void onVerificationRetry(in VerificationSession session);
+ void onVerificationTimeout(int verificationId);
+} \ No newline at end of file
diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.aidl b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl
new file mode 100644
index 000000000000..ac855850a86c
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+/** @hide */
+parcelable VerificationSession;
diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.java b/core/java/android/content/pm/verify/pkg/VerificationSession.java
new file mode 100644
index 000000000000..70b4a022f521
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationSession.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.pm.Flags;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class is used by the system to describe the details about a verification request sent to the
+ * verification agent, aka the verifier. It includes the interfaces for the verifier to communicate
+ * back to the system.
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+@SystemApi
+public final class VerificationSession implements Parcelable {
+ /**
+ * The verification cannot be completed because of unknown reasons.
+ */
+ public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0;
+ /**
+ * The verification cannot be completed because the network is unavailable.
+ */
+ public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1;
+ /**
+ * The verification cannot be completed because the network is limited.
+ */
+ public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = {"VERIFICATION_INCOMPLETE_"}, value = {
+ VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE,
+ VERIFICATION_INCOMPLETE_NETWORK_LIMITED,
+ VERIFICATION_INCOMPLETE_UNKNOWN,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VerificationIncompleteReason {
+ }
+
+ private final int mId;
+ private final int mInstallSessionId;
+ @NonNull
+ private final String mPackageName;
+ @NonNull
+ private final Uri mStagedPackageUri;
+ @NonNull
+ private final SigningInfo mSigningInfo;
+ @NonNull
+ private final List<SharedLibraryInfo> mDeclaredLibraries;
+ @NonNull
+ private final PersistableBundle mExtensionParams;
+ @NonNull
+ private final IVerificationSessionInterface mSession;
+ @NonNull
+ private final IVerificationSessionCallback mCallback;
+
+ /**
+ * Constructor used by the system to describe the details of a verification session.
+ * @hide
+ */
+ public VerificationSession(int id, int installSessionId, @NonNull String packageName,
+ @NonNull Uri stagedPackageUri, @NonNull SigningInfo signingInfo,
+ @NonNull List<SharedLibraryInfo> declaredLibraries,
+ @NonNull PersistableBundle extensionParams,
+ @NonNull IVerificationSessionInterface session,
+ @NonNull IVerificationSessionCallback callback) {
+ mId = id;
+ mInstallSessionId = installSessionId;
+ mPackageName = packageName;
+ mStagedPackageUri = stagedPackageUri;
+ mSigningInfo = signingInfo;
+ mDeclaredLibraries = declaredLibraries;
+ mExtensionParams = extensionParams;
+ mSession = session;
+ mCallback = callback;
+ }
+
+ /**
+ * A unique identifier tied to this specific verification session.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * The package name of the app that is to be verified.
+ */
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * The id of the installation session associated with the verification.
+ */
+ public int getInstallSessionId() {
+ return mInstallSessionId;
+ }
+
+ /**
+ * The Uri of the path where the package's code files are located.
+ */
+ public @NonNull Uri getStagedPackageUri() {
+ return mStagedPackageUri;
+ }
+
+ /**
+ * Signing info of the package to be verified.
+ */
+ public @NonNull SigningInfo getSigningInfo() {
+ return mSigningInfo;
+ }
+
+ /**
+ * Returns a mapping of any shared libraries declared in the manifest
+ * to the {@link SharedLibraryInfo#Type} that is declared. This will be an empty
+ * map if no shared libraries are declared by the package.
+ */
+ @NonNull
+ public List<SharedLibraryInfo> getDeclaredLibraries() {
+ return Collections.unmodifiableList(mDeclaredLibraries);
+ }
+
+ /**
+ * Returns any extension params associated with the verification request.
+ */
+ @NonNull
+ public PersistableBundle getExtensionParams() {
+ return mExtensionParams;
+ }
+
+ /**
+ * Get the value of Clock.elapsedRealtime() at which time this verification
+ * will timeout as incomplete if no other verification response is provided.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public long getTimeoutTime() {
+ try {
+ return mSession.getTimeoutTime(mId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Extend the timeout for this session by the provided additionalMs to
+ * fetch relevant information over the network or wait for the network.
+ * This may be called multiple times. If the request would bypass any max
+ * duration by the system, the method will return a lower value than the
+ * requested amount that indicates how much the time was extended.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public long extendTimeRemaining(long additionalMs) {
+ try {
+ return mSession.extendTimeRemaining(mId, additionalMs);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Report to the system that verification could not be completed along
+ * with an approximate reason to pass on to the installer.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public void reportVerificationIncomplete(@VerificationIncompleteReason int reason) {
+ try {
+ mCallback.reportVerificationIncomplete(mId, reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Report to the system that the verification has completed and the
+ * install process may act on that status to either block in the case
+ * of failure or continue to process the install in the case of success.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public void reportVerificationComplete(@NonNull VerificationStatus status) {
+ try {
+ mCallback.reportVerificationComplete(mId, status);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Same as {@link #reportVerificationComplete(VerificationStatus)}, but also provide
+ * a result to the extension params provided in the request, which will be passed to the
+ * installer in the installation result.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public void reportVerificationComplete(@NonNull VerificationStatus status,
+ @NonNull PersistableBundle response) {
+ try {
+ mCallback.reportVerificationCompleteWithExtensionResponse(mId, status, response);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private VerificationSession(@NonNull Parcel in) {
+ mId = in.readInt();
+ mInstallSessionId = in.readInt();
+ mPackageName = in.readString8();
+ mStagedPackageUri = Uri.CREATOR.createFromParcel(in);
+ mSigningInfo = SigningInfo.CREATOR.createFromParcel(in);
+ mDeclaredLibraries = in.createTypedArrayList(SharedLibraryInfo.CREATOR);
+ mExtensionParams = in.readPersistableBundle(getClass().getClassLoader());
+ mSession = IVerificationSessionInterface.Stub.asInterface(in.readStrongBinder());
+ mCallback = IVerificationSessionCallback.Stub.asInterface(in.readStrongBinder());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mId);
+ dest.writeInt(mInstallSessionId);
+ dest.writeString8(mPackageName);
+ Uri.writeToParcel(dest, mStagedPackageUri);
+ mSigningInfo.writeToParcel(dest, flags);
+ dest.writeTypedList(mDeclaredLibraries);
+ dest.writePersistableBundle(mExtensionParams);
+ dest.writeStrongBinder(mSession.asBinder());
+ dest.writeStrongBinder(mCallback.asBinder());
+ }
+
+ @NonNull
+ public static final Creator<VerificationSession> CREATOR = new Creator<>() {
+ @Override
+ public VerificationSession createFromParcel(@NonNull Parcel in) {
+ return new VerificationSession(in);
+ }
+
+ @Override
+ public VerificationSession[] newArray(int size) {
+ return new VerificationSession[size];
+ }
+ };
+}
diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl
new file mode 100644
index 000000000000..6a1cb4f2d1d2
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+/** @hide */
+parcelable VerificationStatus;
diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.java b/core/java/android/content/pm/verify/pkg/VerificationStatus.java
new file mode 100644
index 000000000000..4d0379d79773
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.content.pm.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class is used by the verifier to describe the status of the verification request, whether
+ * it's successful or it has failed along with any relevant details.
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+public final class VerificationStatus implements Parcelable {
+ /**
+ * The ASL status has not been determined. This happens in situations where the verification
+ * service is not monitoring ASLs, and means the ASL data in the app is not necessarily bad but
+ * can't be trusted.
+ */
+ public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0;
+
+ /**
+ * The app's ASL data is considered to be in a good state.
+ */
+ public static final int VERIFIER_STATUS_ASL_GOOD = 1;
+
+ /**
+ * There is something bad in the app's ASL data; the user should be warned about this when shown
+ * the ASL data and/or appropriate decisions made about the use of this data by the platform.
+ */
+ public static final int VERIFIER_STATUS_ASL_BAD = 2;
+
+ /** @hide */
+ @IntDef(prefix = {"VERIFIER_STATUS_ASL_"}, value = {
+ VERIFIER_STATUS_ASL_UNDEFINED,
+ VERIFIER_STATUS_ASL_GOOD,
+ VERIFIER_STATUS_ASL_BAD,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VerifierStatusAsl {}
+
+ private boolean mIsVerified;
+ private @VerifierStatusAsl int mAslStatus;
+ @NonNull
+ private String mFailuresMessage = "";
+
+ private VerificationStatus() {}
+
+ /**
+ * @return whether the status is set to verified or not.
+ */
+ public boolean isVerified() {
+ return mIsVerified;
+ }
+
+ /**
+ * @return the failure message associated with the failure status.
+ */
+ @NonNull
+ public String getFailureMessage() {
+ return mFailuresMessage;
+ }
+
+ /**
+ * @return the asl status.
+ */
+ public @VerifierStatusAsl int getAslStatus() {
+ return mAslStatus;
+ }
+
+ /**
+ * Builder to construct a {@link VerificationStatus} object.
+ */
+ public static final class Builder {
+ final VerificationStatus mStatus = new VerificationStatus();
+
+ /**
+ * Set in the status whether the verification has succeeded or failed.
+ */
+ @NonNull
+ public Builder setVerified(boolean verified) {
+ mStatus.mIsVerified = verified;
+ return this;
+ }
+
+ /**
+ * Set a developer-facing failure message to include in the verification failure status.
+ */
+ @NonNull
+ public Builder setFailureMessage(@NonNull String failureMessage) {
+ mStatus.mFailuresMessage = failureMessage;
+ return this;
+ }
+
+ /**
+ * Set the ASL status, as defined in {@link VerifierStatusAsl}.
+ */
+ @NonNull
+ public Builder setAslStatus(@VerifierStatusAsl int aslStatus) {
+ mStatus.mAslStatus = aslStatus;
+ return this;
+ }
+
+ /**
+ * Build the status object.
+ */
+ @NonNull
+ public VerificationStatus build() {
+ return mStatus;
+ }
+ }
+
+ private VerificationStatus(Parcel in) {
+ mIsVerified = in.readBoolean();
+ mAslStatus = in.readInt();
+ mFailuresMessage = in.readString8();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mIsVerified);
+ dest.writeInt(mAslStatus);
+ dest.writeString8(mFailuresMessage);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<VerificationStatus> CREATOR = new Creator<>() {
+ @Override
+ public VerificationStatus createFromParcel(@NonNull Parcel in) {
+ return new VerificationStatus(in);
+ }
+
+ @Override
+ public VerificationStatus[] newArray(int size) {
+ return new VerificationStatus[size];
+ }
+ };
+}
diff --git a/core/java/android/content/pm/verify/pkg/VerifierService.java b/core/java/android/content/pm/verify/pkg/VerifierService.java
new file mode 100644
index 000000000000..ccf211915326
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerifierService.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.Flags;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+
+/**
+ * A base service implementation for the verifier agent to implement.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+public abstract class VerifierService extends Service {
+ /**
+ * Called when a package name is available for a pending verification,
+ * giving the verifier opportunity to pre-fetch any relevant information
+ * that may be needed should a verification for the package be required.
+ */
+ public abstract void onPackageNameAvailable(@NonNull String packageName);
+
+ /**
+ * Called when a package recently provided via {@link #onPackageNameAvailable}
+ * is no longer expected to be installed. This is a hint that any pre-fetch or
+ * cache created as a result of the previous call may be be cleared.
+ * <p>This method will never be called after {@link #onVerificationRequired} is called for the
+ * same package. Once a verification is officially requested by
+ * {@link #onVerificationRequired}, it cannot be cancelled.
+ * </p>
+ */
+ public abstract void onVerificationCancelled(@NonNull String packageName);
+
+ /**
+ * Called when an application needs to be verified. Details about the
+ * verification and actions that can be taken on it will be encapsulated in
+ * the provided {@link VerificationSession} parameter.
+ */
+ public abstract void onVerificationRequired(@NonNull VerificationSession session);
+
+ /**
+ * Called when a verification needs to be retried. This can be encountered
+ * when a prior verification was marked incomplete and the user has indicated
+ * that they've resolved the issue, or when a timeout is reached, but the
+ * the system is attempting to retry. Details about the
+ * verification and actions that can be taken on it will be encapsulated in
+ * the provided {@link VerificationSession} parameter.
+ */
+ public abstract void onVerificationRetry(@NonNull VerificationSession session);
+
+ /**
+ * Called in the case that an active verification has failed. Any APIs called
+ * on the {@link VerificationSession} instance associated with this {@code verificationId} will
+ * throw an {@link IllegalStateException}.
+ */
+ public abstract void onVerificationTimeout(int verificationId);
+
+ /**
+ * Called when the verifier service is bound to the system.
+ */
+ public @Nullable IBinder onBind(@Nullable Intent intent) {
+ if (intent == null || !PackageManager.ACTION_VERIFY_PACKAGE.equals(intent.getAction())) {
+ return null;
+ }
+ return new IVerifierService.Stub() {
+ @Override
+ public void onPackageNameAvailable(@NonNull String packageName) {
+ VerifierService.this.onPackageNameAvailable(packageName);
+ }
+
+ @Override
+ public void onVerificationCancelled(@NonNull String packageName) {
+ VerifierService.this.onVerificationCancelled(packageName);
+ }
+
+ @Override
+ public void onVerificationRequired(@NonNull VerificationSession session) {
+ VerifierService.this.onVerificationRequired(session);
+ }
+
+ @Override
+ public void onVerificationRetry(@NonNull VerificationSession session) {
+ VerifierService.this.onVerificationRetry(session);
+ }
+
+ @Override
+ public void onVerificationTimeout(int verificationId) {
+ VerifierService.this.onVerificationTimeout(verificationId);
+ }
+ };
+ }
+}
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index a9c57909e1c1..c9f464716e72 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -25,6 +25,7 @@ import android.util.ArraySet;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Set;
@@ -63,6 +64,7 @@ public class ZenModeDiff {
public static class FieldDiff<T> {
private final T mFrom;
private final T mTo;
+ private final BaseDiff mDetailedDiff;
/**
* Constructor to create a FieldDiff object with the given values.
@@ -72,6 +74,19 @@ public class ZenModeDiff {
public FieldDiff(@Nullable T from, @Nullable T to) {
mFrom = from;
mTo = to;
+ mDetailedDiff = null;
+ }
+
+ /**
+ * Constructor to create a FieldDiff object with the given values, and that has a
+ * detailed BaseDiff.
+ * @param from from (old) value
+ * @param to to (new) value
+ */
+ public FieldDiff(@Nullable T from, @Nullable T to, @Nullable BaseDiff detailedDiff) {
+ mFrom = from;
+ mTo = to;
+ mDetailedDiff = detailedDiff;
}
/**
@@ -93,6 +108,9 @@ public class ZenModeDiff {
*/
@Override
public String toString() {
+ if (mDetailedDiff != null) {
+ return mDetailedDiff.toString();
+ }
return mFrom + "->" + mTo;
}
@@ -100,6 +118,9 @@ public class ZenModeDiff {
* Returns whether this represents an actual diff.
*/
public boolean hasDiff() {
+ if (mDetailedDiff != null) {
+ return mDetailedDiff.hasDiff();
+ }
// note that Objects.equals handles null values gracefully.
return !Objects.equals(mFrom, mTo);
}
@@ -115,7 +136,8 @@ public class ZenModeDiff {
@ExistenceChange private int mExists = NONE;
// Map from field name to diffs for any standalone fields in the object.
- private ArrayMap<String, FieldDiff> mFields = new ArrayMap<>();
+ // LinkedHashMap is specifically chosen here to show insertion order when keys are fetched.
+ private LinkedHashMap<String, FieldDiff> mFields = new LinkedHashMap<>();
// Functions for actually diffing objects and string representations have to be implemented
// by subclasses.
@@ -550,8 +572,16 @@ public class ZenModeDiff {
if (!Objects.equals(from.enabler, to.enabler)) {
addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler));
}
- if (!Objects.equals(from.zenPolicy, to.zenPolicy)) {
- addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy));
+ if (android.app.Flags.modesApi()) {
+ PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy);
+ if (policyDiff.hasDiff()) {
+ addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy,
+ policyDiff));
+ }
+ } else {
+ if (!Objects.equals(from.zenPolicy, to.zenPolicy)) {
+ addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy));
+ }
}
if (from.modified != to.modified) {
addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified));
@@ -560,9 +590,12 @@ public class ZenModeDiff {
addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg));
}
if (android.app.Flags.modesApi()) {
- if (!Objects.equals(from.zenDeviceEffects, to.zenDeviceEffects)) {
+ DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects,
+ to.zenDeviceEffects);
+ if (deviceEffectsDiff.hasDiff()) {
addField(FIELD_ZEN_DEVICE_EFFECTS,
- new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects));
+ new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects,
+ deviceEffectsDiff));
}
if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
addField(FIELD_TRIGGER_DESCRIPTION,
@@ -630,7 +663,7 @@ public class ZenModeDiff {
sb.append(key);
sb.append(":");
- sb.append(diff);
+ sb.append(diff.toString());
}
if (becameActive()) {
diff --git a/core/java/android/view/autofill/AutofillStateFingerprint.java b/core/java/android/view/autofill/AutofillStateFingerprint.java
index 2db4285f0820..7f3858e842ed 100644
--- a/core/java/android/view/autofill/AutofillStateFingerprint.java
+++ b/core/java/android/view/autofill/AutofillStateFingerprint.java
@@ -97,7 +97,6 @@ public final class AutofillStateFingerprint {
if (sDebug) {
Log.d(TAG, "Autofillable views count prior to auth:" + autofillableViews.size());
}
-// ArrayList<Integer> hashes = getFingerprintIds(autofillableViews);
ArrayMap<Integer, View> hashes = getFingerprintIds(autofillableViews);
for (Map.Entry<Integer, View> entry : hashes.entrySet()) {
@@ -123,7 +122,6 @@ public final class AutofillStateFingerprint {
if (view != null) {
int id = getEphemeralFingerprintId(view, 0 /* position irrelevant */);
AutofillId autofillId = view.getAutofillId();
- autofillId.setSessionId(mSessionId);
mHashToAutofillIdMap.put(id, autofillId);
} else {
if (sDebug) {
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index fbc30ed3d8f5..70ac12f07a23 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -306,3 +306,10 @@ flag {
description: "Allow entering desktop mode by default on freeform displays"
bug: "361419732"
}
+
+flag {
+ name: "enable_desktop_app_launch_alttab_transitions"
+ namespace: "lse_desktop_experience"
+ description: "Enables custom transitions for alt-tab app launches in Desktop Mode."
+ bug: "370735595"
+} \ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d35c66ed719e..ed33edecb333 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8449,6 +8449,29 @@
<permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE"
android:protectionLevel="signature"/>
+ <!-- @SystemApi
+ @FlaggedApi("android.content.pm.verification_service")
+ Allows app to be the verification agent to verify packages.
+ <p>Protection level: signature|privileged
+ @hide
+ -->
+ <permission android:name="android.permission.VERIFICATION_AGENT"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.content.pm.verification_service" />
+
+ <!-- @SystemApi
+ @FlaggedApi("android.content.pm.verification_service")
+ Must be required by a privileged {@link android.content.pm.verify.pkg.VerifierService}
+ to ensure that only the system can bind to it.
+ This permission should not be held by anything other than the system.
+ <p>Not for use by third-party applications. </p>
+ <p>Protection level: signature
+ @hide
+ -->
+ <permission android:name="android.permission.BIND_VERIFICATION_AGENT"
+ android:protectionLevel="internal"
+ android:featureFlag="android.content.pm.verification_service" />
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5c0dca2104af..1a3a30d9c36d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1250,6 +1250,7 @@
a watch, setting this config is no-op.
0 - Nothing
1 - Switch to the recent app
+ 2 - Launch the default fitness app
-->
<integer name="config_doublePressOnStemPrimaryBehavior">0</integer>
@@ -2309,10 +2310,6 @@
spatial audio is enabled for a newly connected audio device -->
<bool name="config_spatial_audio_head_tracking_enabled_default">false</bool>
- <!-- Flag indicating whether platform level volume adjustments are enabled for remote sessions
- on grouped devices. -->
- <bool name="config_volumeAdjustmentForRemoteGroupSessions">true</bool>
-
<!-- Flag indicating current media Output Switcher version. -->
<integer name="config_mediaOutputSwitchDialogVersion">1</integer>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 807df1be7fb5..0ccef9123e10 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5102,8 +5102,6 @@
<java-symbol type="dimen" name="config_wallpaperDimAmount" />
- <java-symbol type="bool" name="config_volumeAdjustmentForRemoteGroupSessions" />
-
<java-symbol type="integer" name="config_mediaOutputSwitchDialogVersion" />
<!-- List of shared library packages that should be loaded by the classloader after the
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 9821d433500f..56e18e6c443f 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -250,7 +250,7 @@ android_ravenwood_test {
"androidx.test.rules",
"androidx.test.ext.junit",
"androidx.test.uiautomator_uiautomator",
- "compatibility-device-util-axt",
+ "compatibility-device-util-axt-ravenwood",
"flag-junit",
"platform-test-annotations",
"flag-junit",
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
new file mode 100644
index 000000000000..987f68d4f9e1
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.content.pm.VersionedPackage;
+import android.content.pm.verify.pkg.IVerificationSessionCallback;
+import android.content.pm.verify.pkg.IVerificationSessionInterface;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerificationSessionTest {
+ private static final int TEST_ID = 100;
+ private static final int TEST_INSTALL_SESSION_ID = 33;
+ private static final String TEST_PACKAGE_NAME = "com.foo";
+ private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
+ private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+ private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO1 =
+ new SharedLibraryInfo("sharedLibPath1", TEST_PACKAGE_NAME,
+ Collections.singletonList("path1"), "sharedLib1", 101,
+ SharedLibraryInfo.TYPE_DYNAMIC, new VersionedPackage(TEST_PACKAGE_NAME, 1),
+ null, null, false);
+ private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO2 =
+ new SharedLibraryInfo("sharedLibPath2", TEST_PACKAGE_NAME,
+ Collections.singletonList("path2"), "sharedLib2", 102,
+ SharedLibraryInfo.TYPE_DYNAMIC,
+ new VersionedPackage(TEST_PACKAGE_NAME, 2), null, null, false);
+ private static final long TEST_TIMEOUT_TIME = System.currentTimeMillis();
+ private static final long TEST_EXTEND_TIME = 2000L;
+ private static final String TEST_KEY = "test key";
+ private static final String TEST_VALUE = "test value";
+
+ private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>();
+ private final PersistableBundle mTestExtensionParams = new PersistableBundle();
+ @Mock
+ private IVerificationSessionInterface mTestSessionInterface;
+ @Mock
+ private IVerificationSessionCallback mTestCallback;
+ private VerificationSession mTestSession;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO1);
+ mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO2);
+ mTestExtensionParams.putString(TEST_KEY, TEST_VALUE);
+ mTestSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID,
+ TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, mTestDeclaredLibraries,
+ mTestExtensionParams, mTestSessionInterface, mTestCallback);
+ }
+
+ @Test
+ public void testParcel() {
+ Parcel parcel = Parcel.obtain();
+ mTestSession.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ VerificationSession sessionFromParcel =
+ VerificationSession.CREATOR.createFromParcel(parcel);
+ assertThat(sessionFromParcel.getId()).isEqualTo(TEST_ID);
+ assertThat(sessionFromParcel.getInstallSessionId()).isEqualTo(TEST_INSTALL_SESSION_ID);
+ assertThat(sessionFromParcel.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(sessionFromParcel.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI);
+ assertThat(sessionFromParcel.getSigningInfo().getSigningDetails())
+ .isEqualTo(TEST_SIGNING_INFO.getSigningDetails());
+ List<SharedLibraryInfo> declaredLibrariesFromParcel =
+ sessionFromParcel.getDeclaredLibraries();
+ assertThat(declaredLibrariesFromParcel).hasSize(2);
+ // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly
+ assertThat(declaredLibrariesFromParcel.getFirst().toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString());
+ assertThat(declaredLibrariesFromParcel.get(1).toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString());
+ // We can't directly test with PersistableBundle.equals() because the parceled bundle's
+ // structure is different, but all the key/value pairs should be preserved as before.
+ assertThat(sessionFromParcel.getExtensionParams().getString(TEST_KEY))
+ .isEqualTo(mTestExtensionParams.getString(TEST_KEY));
+ }
+
+ @Test
+ public void testInterface() throws Exception {
+ when(mTestSessionInterface.getTimeoutTime(anyInt())).thenAnswer(i -> TEST_TIMEOUT_TIME);
+ when(mTestSessionInterface.extendTimeRemaining(anyInt(), anyLong())).thenAnswer(
+ i -> i.getArguments()[1]);
+
+ assertThat(mTestSession.getTimeoutTime()).isEqualTo(TEST_TIMEOUT_TIME);
+ verify(mTestSessionInterface, times(1)).getTimeoutTime(eq(TEST_ID));
+ assertThat(mTestSession.extendTimeRemaining(TEST_EXTEND_TIME)).isEqualTo(TEST_EXTEND_TIME);
+ verify(mTestSessionInterface, times(1)).extendTimeRemaining(
+ eq(TEST_ID), eq(TEST_EXTEND_TIME));
+ }
+
+ @Test
+ public void testCallback() throws Exception {
+ PersistableBundle response = new PersistableBundle();
+ response.putString("test key", "test value");
+ final VerificationStatus status =
+ new VerificationStatus.Builder().setVerified(true).build();
+ mTestSession.reportVerificationComplete(status);
+ verify(mTestCallback, times(1)).reportVerificationComplete(
+ eq(TEST_ID), eq(status));
+ mTestSession.reportVerificationComplete(status, response);
+ verify(mTestCallback, times(1))
+ .reportVerificationCompleteWithExtensionResponse(
+ eq(TEST_ID), eq(status), eq(response));
+
+ final int reason = VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN;
+ mTestSession.reportVerificationIncomplete(reason);
+ verify(mTestCallback, times(1)).reportVerificationIncomplete(
+ eq(TEST_ID), eq(reason));
+ }
+}
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java
new file mode 100644
index 000000000000..67d407a72925
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerificationStatusTest {
+ private static final boolean TEST_VERIFIED = true;
+ private static final int TEST_ASL_STATUS = VerificationStatus.VERIFIER_STATUS_ASL_GOOD;
+ private static final String TEST_FAILURE_MESSAGE = "test test";
+ private static final String TEST_KEY = "test key";
+ private static final String TEST_VALUE = "test value";
+ private final PersistableBundle mTestExtras = new PersistableBundle();
+ private VerificationStatus mStatus;
+
+ @Before
+ public void setUpWithBuilder() {
+ mTestExtras.putString(TEST_KEY, TEST_VALUE);
+ mStatus = new VerificationStatus.Builder()
+ .setAslStatus(TEST_ASL_STATUS)
+ .setFailureMessage(TEST_FAILURE_MESSAGE)
+ .setVerified(TEST_VERIFIED)
+ .build();
+ }
+
+ @Test
+ public void testGetters() {
+ assertThat(mStatus.isVerified()).isEqualTo(TEST_VERIFIED);
+ assertThat(mStatus.getAslStatus()).isEqualTo(TEST_ASL_STATUS);
+ assertThat(mStatus.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE);
+ }
+
+ @Test
+ public void testParcel() {
+ Parcel parcel = Parcel.obtain();
+ mStatus.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ VerificationStatus statusFromParcel = VerificationStatus.CREATOR.createFromParcel(parcel);
+ assertThat(statusFromParcel.isVerified()).isEqualTo(TEST_VERIFIED);
+ assertThat(statusFromParcel.getAslStatus()).isEqualTo(TEST_ASL_STATUS);
+ assertThat(statusFromParcel.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE);
+ }
+}
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
new file mode 100644
index 000000000000..7f73a1eb4b48
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.SigningInfo;
+import android.content.pm.verify.pkg.IVerifierService;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerifierService;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerifierServiceTest {
+ private static final int TEST_ID = 100;
+ private static final int TEST_INSTALL_SESSION_ID = 33;
+ private static final String TEST_PACKAGE_NAME = "com.foo";
+ private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
+ private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+ private VerifierService mService;
+ private VerificationSession mSession;
+
+ @Before
+ public void setUp() {
+ mService = Mockito.mock(VerifierService.class, Answers.CALLS_REAL_METHODS);
+ mSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID,
+ TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO,
+ new ArrayList<>(),
+ new PersistableBundle(), null, null);
+ }
+
+ @Test
+ public void testBind() throws Exception {
+ Intent intent = Mockito.mock(Intent.class);
+ when(intent.getAction()).thenReturn(PackageManager.ACTION_VERIFY_PACKAGE);
+ IVerifierService binder =
+ (IVerifierService) mService.onBind(intent);
+ assertThat(binder).isNotNull();
+ binder.onPackageNameAvailable(TEST_PACKAGE_NAME);
+ verify(mService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME));
+ binder.onVerificationCancelled(TEST_PACKAGE_NAME);
+ verify(mService).onVerificationCancelled(eq(TEST_PACKAGE_NAME));
+ binder.onVerificationRequired(mSession);
+ verify(mService).onVerificationRequired(eq(mSession));
+ binder.onVerificationRetry(mSession);
+ verify(mService).onVerificationRetry(eq(mSession));
+ binder.onVerificationTimeout(TEST_ID);
+ verify(mService).onVerificationTimeout(eq(TEST_ID));
+ }
+
+ @Test
+ public void testBindFailsWithWrongIntent() {
+ Intent intent = Mockito.mock(Intent.class);
+ when(intent.getAction()).thenReturn(Intent.ACTION_SEND);
+ assertThat(mService.onBind(intent)).isNull();
+ }
+}
diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp
index cdc8a9e06d0b..7cf49ab5c376 100644
--- a/core/tests/utiltests/Android.bp
+++ b/core/tests/utiltests/Android.bp
@@ -61,7 +61,7 @@ android_ravenwood_test {
"androidx.annotation_annotation",
"androidx.test.rules",
"frameworks-base-testutils",
- "servicestests-utils",
+ "servicestests-utils-ravenwood",
],
srcs: [
"src/android/util/IRemoteMemoryIntArray.aidl",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 955fe83d34ee..985224e30a51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -350,8 +350,17 @@ class DesktopModeTaskRepository (
/** Minimizes the task for [taskId] and [displayId] */
fun minimizeTask(displayId: Int, taskId: Int) {
- logD("Minimize Task: display=%d, task=%d", displayId, taskId)
- desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId)
+ if (displayId == INVALID_DISPLAY) {
+ // When a task vanishes it doesn't have a displayId. Find the display of the task and
+ // mark it as minimized.
+ getDisplayIdForTask(taskId)?.let {
+ minimizeTask(it, taskId)
+ } ?: logW("Minimize task: No display id found for task: taskId=%d", taskId)
+ } else {
+ logD("Minimize Task: display=%d, task=%d", displayId, taskId)
+ desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId)
+ }
+
if (Flags.enableDesktopWindowingPersistence()) {
updatePersistentRepository(displayId)
}
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 125805c14321..b8bb73ba4148 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
@@ -44,7 +44,6 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.DragEvent
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
-import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -550,7 +549,29 @@ class DesktopTasksController(
/** Move a task to the front */
fun moveTaskToFront(taskId: Int) {
- shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) }
+ val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
+ if (task == null) moveBackgroundTaskToFront(taskId) else moveTaskToFront(task)
+ }
+
+ /**
+ * Launch a background task in desktop. Note that this should be used when we are already in
+ * desktop. If outside of desktop and want to launch a background task in desktop, use
+ * [moveBackgroundTaskToDesktop] instead.
+ */
+ private fun moveBackgroundTaskToFront(taskId: Int) {
+ logV("moveBackgroundTaskToFront taskId=%s", taskId)
+ val wct = WindowContainerTransaction()
+ // TODO: b/342378842 - Instead of using default display, support multiple displays
+ val taskToMinimize: RunningTaskInfo? =
+ addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId)
+ wct.startTask(
+ taskId,
+ ActivityOptions.makeBasic().apply {
+ launchWindowingMode = WINDOWING_MODE_FREEFORM
+ }.toBundle(),
+ )
+ val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
+ addPendingMinimizeTransition(transition, taskToMinimize)
}
/** Move a task to the front */
@@ -558,7 +579,8 @@ class DesktopTasksController(
logV("moveTaskToFront taskId=%s", taskInfo.taskId)
val wct = WindowContainerTransaction()
wct.reorder(taskInfo.token, true)
- val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo)
+ val taskToMinimize =
+ addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
addPendingMinimizeTransition(transition, taskToMinimize)
@@ -1254,7 +1276,7 @@ class DesktopTasksController(
}
// Desktop Mode is showing and we're launching a new Task - we might need to minimize
// a Task.
- val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+ val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
if (taskToMinimize != null) {
addPendingMinimizeTransition(transition, taskToMinimize)
return wct
@@ -1280,7 +1302,8 @@ class DesktopTasksController(
// Desktop Mode is already showing and we're launching a new Task - we might need to
// minimize another Task.
- val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+ val taskToMinimize =
+ addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
addPendingMinimizeTransition(transition, taskToMinimize)
}
}
@@ -1313,14 +1336,11 @@ class DesktopTasksController(
// Remove wallpaper activity when the last active task is removed
removeWallpaperActivity(wct)
}
- taskRepository.addClosingTask(task.displayId, task.taskId)
- // If a CLOSE is triggered on a desktop task, remove the task.
- if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() &&
- taskRepository.isVisibleTask(task.taskId) &&
- transitionType == TRANSIT_CLOSE
- ) {
- wct.removeTask(task.token)
+
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+ taskRepository.addClosingTask(task.displayId, task.taskId)
}
+
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(
task.displayId,
@@ -1425,12 +1445,12 @@ class DesktopTasksController(
private fun addAndGetMinimizeChangesIfNeeded(
displayId: Int,
wct: WindowContainerTransaction,
- newTaskInfo: RunningTaskInfo
+ newTaskId: Int
): RunningTaskInfo? {
if (!desktopTasksLimiter.isPresent) return null
return desktopTasksLimiter
.get()
- .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskInfo)
+ .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskId)
}
private fun addPendingMinimizeTransition(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index d84349b1ce1f..7e0741f1f859 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -208,15 +208,15 @@ class DesktopTasksLimiter (
fun addAndGetMinimizeTaskChangesIfNeeded(
displayId: Int,
wct: WindowContainerTransaction,
- newFrontTaskInfo: RunningTaskInfo,
+ newFrontTaskId: Int,
): RunningTaskInfo? {
ProtoLog.v(
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d",
- newFrontTaskInfo.taskId)
+ newFrontTaskId)
val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront(
taskRepository.getActiveNonMinimizedOrderedTasks(displayId),
- newFrontTaskInfo.taskId)
+ newFrontTaskId)
val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack)
if (taskToMinimize != null) {
wct.reorder(taskToMinimize.token, false /* onTop */)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 83cc18baf6cc..7f7f10519bb1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -24,6 +24,7 @@ import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.util.SparseArray;
import android.view.SurfaceControl;
+import android.window.flags.DesktopModeFlags;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -121,7 +122,16 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopModeTaskRepository.ifPresent(repository -> {
- repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
+ // TODO: b/370038902 - Handle Activity#finishAndRemoveTask.
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
+ || repository.isClosingTask(taskInfo.taskId)) {
+ // A task that's vanishing should be removed:
+ // - If it's closed by the X button which means it's marked as a closing task.
+ repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
+ } else {
+ repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, false);
+ repository.minimizeTask(taskInfo.displayId, taskInfo.taskId);
+ }
});
}
mWindowDecorationViewModel.onTaskVanished(taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 1b9bf2acbb96..dc0bc7816859 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -304,54 +304,28 @@ public class PipTransition extends PipTransitionController implements
if (pipChange == null) {
return false;
}
- WindowContainerToken pipTaskToken = pipChange.getContainer();
SurfaceControl pipLeash = pipChange.getLeash();
+ Preconditions.checkNotNull(pipLeash, "Leash is null for swipe-up transition.");
- if (pipTaskToken == null || pipLeash == null) {
- return false;
- }
-
- SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay();
- PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
-
- Rect appBounds = mPipTransitionState.getSwipePipToHomeAppBounds();
- Rect destinationBounds = pipChange.getEndAbsBounds();
-
- float aspectRatio = pipChange.getTaskInfo().pictureInPictureParams.getAspectRatioFloat();
-
- // We fake the source rect hint when the one prvided by the app is invalid for
- // the animation with an app icon overlay.
- Rect animationSrcRectHint = overlayLeash == null ? params.getSourceRectHint()
- : PipUtils.getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio);
-
- WindowContainerTransaction finishWct = new WindowContainerTransaction();
- SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-
- final float scale = (float) destinationBounds.width() / animationSrcRectHint.width();
- startTransaction.setWindowCrop(pipLeash, animationSrcRectHint);
- startTransaction.setPosition(pipLeash,
- destinationBounds.left - animationSrcRectHint.left * scale,
- destinationBounds.top - animationSrcRectHint.top * scale);
- startTransaction.setScale(pipLeash, scale, scale);
-
- if (overlayLeash != null) {
+ final Rect destinationBounds = pipChange.getEndAbsBounds();
+ final SurfaceControl swipePipToHomeOverlay = mPipTransitionState.getSwipePipToHomeOverlay();
+ if (swipePipToHomeOverlay != null) {
final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize(
mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds);
-
- // Overlay needs to be adjusted once a new draw comes in resetting surface transform.
- tx.setScale(overlayLeash, 1f, 1f);
- tx.setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f,
- (destinationBounds.height() - overlaySize) / 2f);
+ // It is possible we reparent the PIP activity to a new PIP task (in multi-activity
+ // apps), so we should also reparent the overlay to the final PIP task.
+ startTransaction.reparent(swipePipToHomeOverlay, pipLeash)
+ .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE)
+ .setScale(swipePipToHomeOverlay, 1f, 1f)
+ .setPosition(swipePipToHomeOverlay,
+ (destinationBounds.width() - overlaySize) / 2f,
+ (destinationBounds.height() - overlaySize) / 2f);
}
- startTransaction.apply();
- tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
- this::onClientDrawAtTransitionEnd);
- finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
-
- // Note that finishWct should be free of any actual WM state changes; we are using
- // it for syncing with the client draw after delayed configuration changes are dispatched.
- finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct);
+ startTransaction.merge(finishTransaction);
+ startTransaction.apply();
+ finishCallback.onTransitionFinished(null /* finishWct */);
+ onClientDrawAtTransitionEnd();
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 8077aeebf27f..f7ed1dd4606b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -52,7 +52,6 @@ import android.util.ArrayMap;
import android.util.IntArray;
import android.util.Pair;
import android.util.Slog;
-import android.view.Display;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.PictureInPictureSurfaceTransaction;
@@ -910,6 +909,14 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
"task #" + taskInfo.taskId + " is always_on_top");
return;
}
+ if (TransitionUtil.isClosingType(change.getMode())
+ && taskInfo != null && taskInfo.lastParentTaskIdBeforePip > 0) {
+ // Pinned task is closing as a side effect of the removal of its original Task,
+ // such transition should be handled by PiP. So cancel the merge here.
+ cancel(false /* toHome */, false /* withScreenshots */,
+ "task #" + taskInfo.taskId + " is removed with its original parent");
+ return;
+ }
final boolean isRootTask = taskInfo != null
&& TransitionInfo.isIndependent(change, info);
final boolean isRecentsTask = mRecentsTask != null
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 794f9d819f4b..97ceecc3662d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -820,6 +820,18 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
}
@Test
+ fun minimizeTask_withInvalidDisplay_minimizesCorrectTask() {
+ repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 0)
+ repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 0)
+
+ repo.minimizeTask(displayId = INVALID_DISPLAY, taskId = 0)
+
+ assertThat(repo.isMinimizedTask(taskId = 0)).isTrue()
+ assertThat(repo.isMinimizedTask(taskId = 1)).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 2)).isFalse()
+ }
+
+ @Test
fun unminimizeTask_unminimizesTask() {
repo.minimizeTask(displayId = 0, taskId = 0)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 8870846efed4..2ddb1acf7300 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1350,6 +1350,32 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ fun moveTaskToFront_backgroundTask_launchesTask() {
+ val task = createTaskInfo(1)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+
+ controller.moveTaskToFront(task.taskId)
+
+ val wct = getLatestWct(type = TRANSIT_OPEN)
+ assertThat(wct.hierarchyOps).hasSize(1)
+ wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() {
+ val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+ val task = createTaskInfo(1001)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+
+ controller.moveTaskToFront(task.taskId)
+
+ val wct = getLatestWct(type = TRANSIT_OPEN)
+ assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize
+ wct.assertReorderAt(0, freeformTasks[0], toTop = false)
+ wct.assertLaunchTaskAt(1, task.taskId, WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
fun moveToNextDisplay_noOtherDisplays() {
whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2075,11 +2101,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
- )
- fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -2112,8 +2135,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
+ fun handleRequest_backTransition_singleTaskNoToken_doesNotHandle() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -2122,11 +2144,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
taskRepository.wallpaperActivityToken = MockToken().token()
@@ -2149,11 +2168,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_multipleTasks_noWallpaper_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2165,7 +2181,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() {
+ fun handleRequest_backTransition_multipleTasks_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2211,11 +2227,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_withBackNav_removesWallpaper() {
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_removesWallpaper() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val wallpaperToken = MockToken().token()
@@ -2231,11 +2244,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -2244,22 +2254,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() {
- val task = setUpFreeformTask()
-
- val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
+ fun handleRequest_closeTransition_singleTaskNoToken_doesNotHandle() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -2268,11 +2264,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
taskRepository.wallpaperActivityToken = MockToken().token()
@@ -2282,26 +2275,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_singleTaskWithToken_removesWallpaperAndTask() {
- val task = setUpFreeformTask()
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_noBackNav_removesWallpaper() {
+ fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() {
val task = setUpFreeformTask()
val wallpaperToken = MockToken().token()
@@ -2313,11 +2288,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2328,25 +2300,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_multipleTasks_withWallpaper_withBackNav_removesTask() {
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
-
- taskRepository.wallpaperActivityToken = MockToken().token()
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
- assertNotNull(result, "Should handle request")
- result.assertRemoveAt(index = 0, task1.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_multipleTasksFlagEnabled_noBackNav_doesNotHandle() {
+ fun handleRequest_closeTransition_multipleTasksFlagEnabled_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2357,28 +2312,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() {
- val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task1.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() {
+ fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val wallpaperToken = MockToken().token()
@@ -2392,28 +2327,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_multipleTasksOneNonMinimized_removesWallpaperAndTask() {
- val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task1.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() {
+ fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val wallpaperToken = MockToken().token()
@@ -2427,11 +2342,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_minimizadTask_withWallpaper_withBackNav_removesWallpaper() {
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val wallpaperToken = MockToken().token()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 045e07796cb8..bc5ae97a55ea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -291,7 +291,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
displayId = DEFAULT_DISPLAY,
wct = wct,
- newFrontTaskInfo = setUpFreeformTask())
+ newFrontTaskId = setUpFreeformTask().taskId)
assertThat(minimizedTaskId).isNull()
assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
@@ -307,7 +307,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
displayId = DEFAULT_DISPLAY,
wct = wct,
- newFrontTaskInfo = setUpFreeformTask())
+ newFrontTaskId = setUpFreeformTask().taskId)
assertThat(minimizedTaskId).isEqualTo(tasks.first())
assertThat(wct.hierarchyOps.size).isEqualTo(1)
@@ -325,7 +325,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
displayId = 0,
wct = wct,
- newFrontTaskInfo = setUpFreeformTask())
+ newFrontTaskId = setUpFreeformTask().taskId)
assertThat(minimizedTaskId).isNull()
assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 763d0153071e..3b2c7e6eaf99 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -18,15 +18,19 @@ package com.android.wm.shell.freeform;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.INVALID_DISPLAY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.platform.test.annotations.EnableFlags;
import android.view.SurfaceControl;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -139,6 +143,40 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
verify(mLaunchAdjacentController).setLaunchAdjacentEnabled(true);
}
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+ public void onTaskVanished_nonClosingTask_isMinimized() {
+ ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ task.isVisible = true;
+
+ mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl);
+
+ task.isVisible = false;
+ task.displayId = INVALID_DISPLAY;
+ mFreeformTaskListener.onTaskVanished(task);
+
+ verify(mDesktopModeTaskRepository).minimizeTask(task.displayId, task.taskId);
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+ public void onTaskVanished_closingTask_isNotMinimized() {
+ ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ task.isVisible = true;
+
+ mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl);
+
+ when(mDesktopModeTaskRepository.isClosingTask(task.taskId)).thenReturn(true);
+ task.isVisible = false;
+ task.displayId = INVALID_DISPLAY;
+ mFreeformTaskListener.onTaskVanished(task);
+
+ verify(mDesktopModeTaskRepository, never()).minimizeTask(task.displayId, task.taskId);
+ verify(mDesktopModeTaskRepository).removeFreeformTask(task.displayId, task.taskId);
+ }
+
@After
public void tearDown() {
mMockitoSession.finishMocking();
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index 9899e4ec388d..83a4dd5a682a 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -22,7 +22,6 @@ import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.res.Resources;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -57,8 +56,6 @@ public final class RoutingSessionInfo implements Parcelable {
}
};
- private static final String TAG = "RoutingSessionInfo";
-
private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE";
private static final String KEY_VOLUME_HANDLING = "volumeHandling";
@@ -142,15 +139,7 @@ public final class RoutingSessionInfo implements Parcelable {
mVolume = builder.mVolume;
mIsSystemSession = builder.mIsSystemSession;
-
- boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
- mVolumeHandling =
- defineVolumeHandling(
- mIsSystemSession,
- builder.mVolumeHandling,
- mSelectedRoutes,
- volumeAdjustmentForRemoteGroupSessions);
+ mVolumeHandling = builder.mVolumeHandling;
mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling);
mTransferReason = builder.mTransferReason;
@@ -207,20 +196,6 @@ public final class RoutingSessionInfo implements Parcelable {
return controlHints;
}
- @MediaRoute2Info.PlaybackVolume
- private static int defineVolumeHandling(
- boolean isSystemSession,
- @MediaRoute2Info.PlaybackVolume int volumeHandling,
- List<String> selectedRoutes,
- boolean volumeAdjustmentForRemoteGroupSessions) {
- if (!isSystemSession
- && !volumeAdjustmentForRemoteGroupSessions
- && selectedRoutes.size() > 1) {
- return MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
- }
- return volumeHandling;
- }
-
@NonNull
private static String ensureString(@Nullable String str) {
return str != null ? str : "";
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
index 3955ff068d94..5f5058d79545 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
@@ -18,8 +18,6 @@ package com.android.mediaroutertest;
import static com.google.common.truth.Truth.assertThat;
-import android.content.res.Resources;
-import android.media.MediaRoute2Info;
import android.media.RoutingSessionInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -95,24 +93,4 @@ public class RoutingSessionInfoTest {
assertThat(sessionInfoWithProviderId2.getTransferableRoutes())
.isEqualTo(sessionInfoWithProviderId.getTransferableRoutes());
}
-
- @Test
- public void testGetVolumeHandlingGroupSession() {
- RoutingSessionInfo sessionInfo = new RoutingSessionInfo.Builder(
- TEST_ID, TEST_CLIENT_PACKAGE_NAME)
- .setName(TEST_NAME)
- .addSelectedRoute(TEST_ROUTE_ID_0)
- .addSelectedRoute(TEST_ROUTE_ID_2)
- .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
- .build();
-
- boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
-
- int expectedResult = volumeAdjustmentForRemoteGroupSessions
- ? MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE :
- MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
-
- assertThat(sessionInfo.getVolumeHandling()).isEqualTo(expectedResult);
- }
}
diff --git a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml
deleted file mode 100644
index a1761e55f1e0..000000000000
--- a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2024 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<resources>
- <declare-styleable name="ButtonPreference">
- <attr name="buttonType" format="enum">
- <enum name="filled" value="0"/>
- <enum name="tonal" value="1"/>
- <enum name="outline" value="2"/>
- </attr>
- <attr name="buttonSize" format="enum">
- <enum name="normal" value="0"/>
- <enum name="large" value="1"/>
- <enum name="extra" value="2"/>
- </attr>
- </declare-styleable>
-</resources> \ No newline at end of file
diff --git a/packages/SettingsLib/ButtonPreference/res/values/attrs.xml b/packages/SettingsLib/ButtonPreference/res/values/attrs.xml
index 9c1e503f1372..970eeb2bfa72 100644
--- a/packages/SettingsLib/ButtonPreference/res/values/attrs.xml
+++ b/packages/SettingsLib/ButtonPreference/res/values/attrs.xml
@@ -18,12 +18,12 @@
<resources>
<declare-styleable name="ButtonPreference">
<attr name="android:gravity" />
- <attr name="buttonType" format="enum">
+ <attr name="buttonPreferenceType" format="enum">
<enum name="filled" value="0"/>
<enum name="tonal" value="1"/>
<enum name="outline" value="2"/>
</attr>
- <attr name="buttonSize" format="enum">
+ <attr name="buttonPreferenceSize" format="enum">
<enum name="normal" value="0"/>
<enum name="large" value="1"/>
<enum name="extra" value="2"/>
diff --git a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
index 0041eb2c7072..979ff96be3f7 100644
--- a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
+++ b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
@@ -137,8 +137,8 @@ public class ButtonPreference extends Preference {
mGravity = a.getInt(R.styleable.ButtonPreference_android_gravity, Gravity.START);
if (SettingsThemeHelper.isExpressiveTheme(context)) {
- int type = a.getInt(R.styleable.ButtonPreference_buttonType, 0);
- int size = a.getInt(R.styleable.ButtonPreference_buttonSize, 0);
+ int type = a.getInt(R.styleable.ButtonPreference_buttonPreferenceType, 0);
+ int size = a.getInt(R.styleable.ButtonPreference_buttonPreferenceSize, 0);
resId = ButtonStyle.getLayoutId(type, size);
}
a.recycle();
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index f6e10570e656..0773bd701d5c 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -186,7 +186,7 @@ public class GlobalSettingsValidators {
VALIDATORS.put(
Global.STEM_PRIMARY_BUTTON_SHORT_PRESS, new InclusiveIntegerRangeValidator(0, 1));
VALIDATORS.put(
- Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS, new InclusiveIntegerRangeValidator(0, 1));
+ Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS, new InclusiveIntegerRangeValidator(0, 2));
VALIDATORS.put(
Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS, new InclusiveIntegerRangeValidator(0, 1));
VALIDATORS.put(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index dc9e267cb0e1..56de096effce 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -132,7 +132,6 @@ fun SceneContainer(
state = state,
modifier = modifier.fillMaxSize(),
swipeSourceDetector = viewModel.edgeDetector,
- gestureFilter = viewModel::shouldFilterGesture,
) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index c8adac064690..5fa5db880cce 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -124,10 +124,6 @@ internal class DraggableHandlerImpl(
overSlop: Float,
pointersDown: Int,
): DragController {
- if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) {
- return NoOpDragController
- }
-
if (overSlop == 0f) {
val oldDragController = dragController
check(oldDragController != null && oldDragController.isDrivingTransition) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index dc3135ddbf54..5ddc28485ac6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -42,10 +42,8 @@ import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.PointerInputModifierNode
import androidx.compose.ui.node.currentValueOf
-import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
@@ -79,7 +77,6 @@ import kotlinx.coroutines.launch
@Stable
internal fun Modifier.multiPointerDraggable(
orientation: Orientation,
- enabled: () -> Boolean,
startDragImmediately: (startedPosition: Offset) -> Boolean,
onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
onFirstPointerDown: () -> Unit = {},
@@ -89,7 +86,6 @@ internal fun Modifier.multiPointerDraggable(
this.then(
MultiPointerDraggableElement(
orientation,
- enabled,
startDragImmediately,
onDragStarted,
onFirstPointerDown,
@@ -100,7 +96,6 @@ internal fun Modifier.multiPointerDraggable(
private data class MultiPointerDraggableElement(
private val orientation: Orientation,
- private val enabled: () -> Boolean,
private val startDragImmediately: (startedPosition: Offset) -> Boolean,
private val onDragStarted:
(startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
@@ -111,7 +106,6 @@ private data class MultiPointerDraggableElement(
override fun create(): MultiPointerDraggableNode =
MultiPointerDraggableNode(
orientation = orientation,
- enabled = enabled,
startDragImmediately = startDragImmediately,
onDragStarted = onDragStarted,
onFirstPointerDown = onFirstPointerDown,
@@ -121,7 +115,6 @@ private data class MultiPointerDraggableElement(
override fun update(node: MultiPointerDraggableNode) {
node.orientation = orientation
- node.enabled = enabled
node.startDragImmediately = startDragImmediately
node.onDragStarted = onDragStarted
node.onFirstPointerDown = onFirstPointerDown
@@ -131,7 +124,6 @@ private data class MultiPointerDraggableElement(
internal class MultiPointerDraggableNode(
orientation: Orientation,
- enabled: () -> Boolean,
var startDragImmediately: (startedPosition: Offset) -> Boolean,
var onDragStarted:
(startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
@@ -142,21 +134,10 @@ internal class MultiPointerDraggableNode(
DelegatingNode(),
PointerInputModifierNode,
CompositionLocalConsumerModifierNode,
- ObserverModifierNode,
SpaceVectorConverter {
private val pointerTracker = delegate(SuspendingPointerInputModifierNode { pointerTracker() })
private val pointerInput = delegate(SuspendingPointerInputModifierNode { pointerInput() })
private val velocityTracker = VelocityTracker()
- private var previousEnabled: Boolean = false
-
- var enabled: () -> Boolean = enabled
- set(value) {
- // Reset the pointer input whenever enabled changed.
- if (value != field) {
- field = value
- pointerInput.resetPointerInputHandler()
- }
- }
private var converter = SpaceVectorConverter(orientation)
@@ -178,21 +159,6 @@ internal class MultiPointerDraggableNode(
}
}
- override fun onAttach() {
- previousEnabled = enabled()
- onObservedReadsChanged()
- }
-
- override fun onObservedReadsChanged() {
- observeReads {
- val newEnabled = enabled()
- if (newEnabled != previousEnabled) {
- pointerInput.resetPointerInputHandler()
- }
- previousEnabled = newEnabled
- }
- }
-
override fun onCancelPointerInput() {
pointerTracker.onCancelPointerInput()
pointerInput.onCancelPointerInput()
@@ -254,9 +220,7 @@ internal class MultiPointerDraggableNode(
velocityTracker.resetTracking()
velocityTracker.addPointerInputChange(firstPointerDown)
startedPosition = firstPointerDown.position
- if (enabled()) {
- onFirstPointerDown()
- }
+ onFirstPointerDown()
}
// Changes with at least one pointer
@@ -295,10 +259,6 @@ internal class MultiPointerDraggableNode(
}
private suspend fun PointerInputScope.pointerInput() {
- if (!enabled()) {
- return
- }
-
val currentContext = currentCoroutineContext()
awaitPointerEventScope {
while (currentContext.isActive) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 6e89814a2dc2..cec888380513 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -47,9 +47,6 @@ import androidx.compose.ui.unit.LayoutDirection
* @param state the state of this layout.
* @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from,
* if any.
- * @param gestureFilter decides whether a drag gesture that started at the given start position
- * should be filtered. If the lambda returns `true`, the drag gesture will be ignored. If it
- * returns `false`, the drag gesture will be handled.
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param builder the configuration of the different scenes and overlays of this layout.
@@ -60,7 +57,6 @@ fun SceneTransitionLayout(
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
- gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
builder: SceneTransitionLayoutScope.() -> Unit,
) {
@@ -69,7 +65,6 @@ fun SceneTransitionLayout(
modifier,
swipeSourceDetector,
swipeDetector,
- gestureFilter,
transitionInterceptionThreshold,
onLayoutImpl = null,
builder,
@@ -621,7 +616,6 @@ internal fun SceneTransitionLayoutForTesting(
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
- gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
builder: SceneTransitionLayoutScope.() -> Unit,
@@ -638,7 +632,6 @@ internal fun SceneTransitionLayoutForTesting(
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = builder,
animationScope = animationScope,
- gestureFilter = gestureFilter,
)
.also { onLayoutImpl?.invoke(it) }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 9e7be37523f4..65c404387734 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -31,7 +31,6 @@ import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
@@ -71,7 +70,6 @@ internal class SceneTransitionLayoutImpl(
* animations.
*/
internal val animationScope: CoroutineScope,
- internal val gestureFilter: (startedPosition: Offset) -> Boolean,
) {
/**
* The map of [Scene]s.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
index f758102fee47..54ee78366875 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
@@ -17,7 +17,6 @@
package com.android.compose.animation.scene
import androidx.compose.runtime.Stable
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
/** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */
@@ -32,8 +31,6 @@ interface SwipeDetector {
val DefaultSwipeDetector = PassthroughSwipeDetector()
-val DefaultGestureFilter = { _: Offset -> false }
-
/** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */
class PassthroughSwipeDetector : SwipeDetector {
override fun detectSwipe(change: PointerInputChange): Boolean {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 98d4aaa91458..d201be9acc12 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -41,7 +41,28 @@ internal fun Modifier.swipeToScene(
draggableHandler: DraggableHandlerImpl,
swipeDetector: SwipeDetector,
): Modifier {
- return this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
+ return if (draggableHandler.enabled()) {
+ this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
+ } else {
+ this
+ }
+}
+
+private fun DraggableHandlerImpl.enabled(): Boolean {
+ return isDrivingTransition || contentForSwipes().shouldEnableSwipes(orientation)
+}
+
+private fun DraggableHandlerImpl.contentForSwipes(): Content {
+ return layoutImpl.contentForUserActions()
+}
+
+/** Whether swipe should be enabled in the given [orientation]. */
+private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
+ if (userActions.isEmpty()) {
+ return false
+ }
+
+ return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
}
private data class SwipeToSceneElement(
@@ -64,7 +85,6 @@ private class SwipeToSceneNode(
delegate(
MultiPointerDraggableNode(
orientation = draggableHandler.orientation,
- enabled = ::enabled,
startDragImmediately = ::startDragImmediately,
onDragStarted = draggableHandler::onDragStarted,
onFirstPointerDown = ::onFirstPointerDown,
@@ -124,22 +144,6 @@ private class SwipeToSceneNode(
override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()
- private fun enabled(): Boolean {
- return draggableHandler.isDrivingTransition ||
- contentForSwipes().shouldEnableSwipes(multiPointerDraggableNode.orientation)
- }
-
- private fun contentForSwipes(): Content {
- return draggableHandler.layoutImpl.contentForUserActions()
- }
-
- /** Whether swipe should be enabled in the given [orientation]. */
- private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
- return userActions.keys.any {
- it is Swipe.Resolved && it.direction.orientation == orientation
- }
- }
-
private fun startDragImmediately(startedPosition: Offset): Boolean {
// Immediately start the drag if the user can't swipe in the other direction and the gesture
// handler can intercept it.
@@ -152,7 +156,7 @@ private class SwipeToSceneNode(
Orientation.Vertical -> Orientation.Horizontal
Orientation.Horizontal -> Orientation.Vertical
}
- return contentForSwipes().shouldEnableSwipes(oppositeOrientation)
+ return draggableHandler.contentForSwipes().shouldEnableSwipes(oppositeOrientation)
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 16dc0d544fd2..dd4f99f5c64e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -109,8 +109,6 @@ class DraggableHandlerTest {
val transitionInterceptionThreshold = 0.05f
- var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter
-
private val layoutImpl =
SceneTransitionLayoutImpl(
state = layoutState,
@@ -123,7 +121,6 @@ class DraggableHandlerTest {
// Use testScope and not backgroundScope here because backgroundScope does not
// work well with advanceUntilIdle(), which is used by some tests.
animationScope = testScope,
- gestureFilter = { startedPosition -> gestureFilter.invoke(startedPosition) },
)
.apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
@@ -352,13 +349,6 @@ class DraggableHandlerTest {
}
@Test
- fun onDragStarted_doesNotStartTransition_whenGestureFiltered() = runGestureTest {
- gestureFilter = { _ -> true }
- onDragStarted(overSlop = down(fractionOfScreen = 0.1f), expectedConsumedOverSlop = 0f)
- assertIdle(currentScene = SceneA)
- }
-
- @Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
assertTransition(currentScene = SceneA)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
index 493f3a1377cc..c8f6e6d99933 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
@@ -45,6 +45,7 @@ import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Velocity
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.modifiers.thenIf
import com.android.compose.nestedscroll.SuspendedValue
import com.google.common.truth.Truth.assertThat
import kotlin.properties.Delegates
@@ -94,19 +95,20 @@ class MultiPointerDraggableTest {
Box(
Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
.nestedScrollDispatcher()
- .multiPointerDraggable(
- orientation = Orientation.Vertical,
- enabled = { enabled },
- startDragImmediately = { false },
- onDragStarted = { _, _, _ ->
- started = true
- SimpleDragController(
- onDrag = { dragged = true },
- onStop = { stopped = true },
- )
- },
- dispatcher = defaultDispatcher,
- )
+ .thenIf(enabled) {
+ Modifier.multiPointerDraggable(
+ orientation = Orientation.Vertical,
+ startDragImmediately = { false },
+ onDragStarted = { _, _, _ ->
+ started = true
+ SimpleDragController(
+ onDrag = { dragged = true },
+ onStop = { stopped = true },
+ )
+ },
+ dispatcher = defaultDispatcher,
+ )
+ }
)
}
@@ -164,7 +166,6 @@ class MultiPointerDraggableTest {
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
// We want to start a drag gesture immediately
startDragImmediately = { true },
onDragStarted = { _, _, _ ->
@@ -238,7 +239,6 @@ class MultiPointerDraggableTest {
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
started = true
@@ -358,7 +358,6 @@ class MultiPointerDraggableTest {
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
started = true
@@ -464,7 +463,6 @@ class MultiPointerDraggableTest {
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
verticalStarted = true
@@ -477,7 +475,6 @@ class MultiPointerDraggableTest {
)
.multiPointerDraggable(
orientation = Orientation.Horizontal,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
horizontalStarted = true
@@ -570,7 +567,6 @@ class MultiPointerDraggableTest {
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
swipeDetector =
object : SwipeDetector {
@@ -672,7 +668,6 @@ class MultiPointerDraggableTest {
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
SimpleDragController(
@@ -744,7 +739,6 @@ class MultiPointerDraggableTest {
.nestedScrollDispatcher()
.multiPointerDraggable(
orientation = Orientation.Vertical,
- enabled = { true },
startDragImmediately = { false },
onDragStarted = { _, _, _ ->
SimpleDragController(
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 25e87132eb0e..ce64628a863c 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -22,11 +22,15 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@@ -36,8 +40,11 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeWithVelocity
import androidx.compose.ui.unit.Density
@@ -844,4 +851,29 @@ class SwipeToSceneTest {
assertThat(transition.progress).isEqualTo(1f)
assertThat(availableOnPostScroll).isEqualTo(ovescrollPx)
}
+
+ @Test
+ fun sceneWithoutSwipesDoesNotConsumeGestures() {
+ val buttonTag = "button"
+
+ rule.setContent {
+ Box {
+ var count by remember { mutableStateOf(0) }
+ Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) {
+ Text("Count: $count")
+ }
+
+ SceneTransitionLayout(remember { MutableSceneTransitionLayoutState(SceneA) }) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+ }
+
+ rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0")
+
+ // Click on the root at its center, where the button is located. Clicks should go through
+ // the STL and reach the button given that there is no swipes for the current scene.
+ repeat(3) { rule.onRoot().performClick() }
+ rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3")
+ }
}
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
index c399abc7d81d..81d92faa40af 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -36,6 +36,7 @@ android_library {
"SystemUIPluginLib",
"SystemUIUnfoldLib",
"kotlinx_coroutines",
+ "monet",
"dagger2",
"jsr330",
],
diff --git a/packages/SystemUI/customization/res/values/ids.xml b/packages/SystemUI/customization/res/values/ids.xml
index 5eafbfc1f0b1..ec466f041179 100644
--- a/packages/SystemUI/customization/res/values/ids.xml
+++ b/packages/SystemUI/customization/res/values/ids.xml
@@ -6,4 +6,13 @@
<item type="id" name="weather_clock_weather_icon" />
<item type="id" name="weather_clock_temperature" />
<item type="id" name="weather_clock_alarm_dnd" />
+
+ <item type="id" name="HOUR_DIGIT_PAIR"/>
+ <item type="id" name="MINUTE_DIGIT_PAIR"/>
+ <item type="id" name="HOUR_FIRST_DIGIT"/>
+ <item type="id" name="HOUR_SECOND_DIGIT"/>
+ <item type="id" name="MINUTE_FIRST_DIGIT"/>
+ <item type="id" name="MINUTE_SECOND_DIGIT"/>
+ <item type="id" name="TIME_FULL_FORMAT"/>
+ <item type="id" name="DATE_FORMAT"/>
</resources> \ No newline at end of file
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 1863cd861c05..9877406eeac2 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -62,6 +62,7 @@ constructor(
// implement the get method and ensure a value is returned before initialization is complete.
private var logger = DEFAULT_LOGGER
get() = field ?: DEFAULT_LOGGER
+
var messageBuffer: MessageBuffer
get() = logger.buffer
set(value) {
@@ -123,24 +124,24 @@ constructor(
attrs,
R.styleable.AnimatableClockView,
defStyleAttr,
- defStyleRes
+ defStyleRes,
)
try {
dozingWeightInternal =
animatableClockViewAttributes.getInt(
R.styleable.AnimatableClockView_dozeWeight,
- /* default = */ 100
+ /* default = */ 100,
)
lockScreenWeightInternal =
animatableClockViewAttributes.getInt(
R.styleable.AnimatableClockView_lockScreenWeight,
- /* default = */ 300
+ /* default = */ 300,
)
chargeAnimationDelay =
animatableClockViewAttributes.getInt(
R.styleable.AnimatableClockView_chargeAnimationDelay,
- /* default = */ 200
+ /* default = */ 200,
)
} finally {
animatableClockViewAttributes.recycle()
@@ -151,14 +152,14 @@ constructor(
attrs,
android.R.styleable.TextView,
defStyleAttr,
- defStyleRes
+ defStyleRes,
)
try {
isSingleLineInternal =
textViewAttributes.getBoolean(
android.R.styleable.TextView_singleLine,
- /* default = */ false
+ /* default = */ false,
)
} finally {
textViewAttributes.recycle()
@@ -280,7 +281,7 @@ constructor(
text: CharSequence,
start: Int,
lengthBefore: Int,
- lengthAfter: Int
+ lengthAfter: Int,
) {
logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() }
super.onTextChanged(text, start, lengthBefore, lengthAfter)
@@ -305,7 +306,7 @@ constructor(
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = lockScreenWeight,
@@ -314,7 +315,7 @@ constructor(
interpolator = null,
duration = COLOR_ANIM_DURATION,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -327,7 +328,7 @@ constructor(
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = lockScreenWeight,
@@ -336,7 +337,7 @@ constructor(
duration = APPEAR_ANIM_DURATION,
interpolator = Interpolators.EMPHASIZED_DECELERATE,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -353,7 +354,7 @@ constructor(
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = dozingWeightInternal,
@@ -362,7 +363,7 @@ constructor(
interpolator = Interpolators.EMPHASIZED_DECELERATE,
duration = ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -381,7 +382,7 @@ constructor(
interpolator = null,
duration = CHARGE_ANIM_DURATION_PHASE_1,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
setTextStyle(
@@ -391,7 +392,7 @@ constructor(
interpolator = null,
duration = CHARGE_ANIM_DURATION_PHASE_0,
delay = chargeAnimationDelay.toLong(),
- onAnimationEnd = startAnimPhase2
+ onAnimationEnd = startAnimPhase2,
)
}
@@ -404,7 +405,7 @@ constructor(
interpolator = null,
duration = DOZE_ANIM_DURATION,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -444,7 +445,7 @@ constructor(
interpolator: TimeInterpolator?,
duration: Long,
delay: Long,
- onAnimationEnd: Runnable?
+ onAnimationEnd: Runnable?,
) {
textAnimator?.let {
it.setTextStyle(
@@ -454,7 +455,7 @@ constructor(
duration = duration,
interpolator = interpolator,
delay = delay,
- onAnimationEnd = onAnimationEnd
+ onAnimationEnd = onAnimationEnd,
)
it.glyphFilter = glyphFilter
}
@@ -468,7 +469,7 @@ constructor(
duration = duration,
interpolator = interpolator,
delay = delay,
- onAnimationEnd = onAnimationEnd
+ onAnimationEnd = onAnimationEnd,
)
textAnimator.glyphFilter = glyphFilter
}
@@ -476,6 +477,7 @@ constructor(
}
fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
+
fun refreshFormat(use24HourFormat: Boolean) {
Patterns.update(context)
@@ -560,18 +562,11 @@ constructor(
* @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
* it finished moving.
*/
- fun offsetGlyphsForStepClockAnimation(
- distance: Float,
- fraction: Float,
- ) {
+ fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) {
for (i in 0 until NUM_DIGITS) {
val dir = if (isLayoutRtl) -1 else 1
val digitFraction =
- getDigitFraction(
- digit = i,
- isMovingToCenter = distance > 0,
- fraction = fraction,
- )
+ getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction)
val moveAmountForDigit = dir * distance * digitFraction
glyphOffsets[i] = moveAmountForDigit
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
new file mode 100644
index 000000000000..d001ef966c13
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
@@ -0,0 +1,448 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.util.TypedValue
+import com.android.internal.graphics.ColorUtils
+import com.android.internal.graphics.cam.Cam
+import com.android.internal.graphics.cam.CamUtils
+import com.android.internal.policy.SystemBarUtils
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.monet.Style as MonetStyle
+import com.android.systemui.monet.TonalPalette
+import java.io.IOException
+import kotlin.math.abs
+
+class AssetLoader
+private constructor(
+ private val pluginCtx: Context,
+ private val sysuiCtx: Context,
+ private val baseDir: String,
+ var colorScheme: ColorScheme?,
+ var seedColor: Int?,
+ var overrideChroma: Float?,
+ val typefaceCache: TypefaceCache,
+ val getThemeSeedColor: (Context) -> Int,
+ messageBuffer: MessageBuffer,
+) {
+ val logger = Logger(messageBuffer, TAG)
+ private val resources =
+ listOf(
+ Pair(pluginCtx.resources, pluginCtx.packageName),
+ Pair(sysuiCtx.resources, sysuiCtx.packageName),
+ )
+
+ constructor(
+ pluginCtx: Context,
+ sysuiCtx: Context,
+ baseDir: String,
+ messageBuffer: MessageBuffer,
+ getThemeSeedColor: ((Context) -> Int)? = null,
+ ) : this(
+ pluginCtx,
+ sysuiCtx,
+ baseDir,
+ colorScheme = null,
+ seedColor = null,
+ overrideChroma = null,
+ typefaceCache =
+ TypefaceCache(messageBuffer) { Typeface.createFromAsset(pluginCtx.assets, it) },
+ getThemeSeedColor = getThemeSeedColor ?: Companion::getThemeSeedColor,
+ messageBuffer = messageBuffer,
+ )
+
+ fun listAssets(path: String): List<String> {
+ return pluginCtx.resources.assets.list("$baseDir$path")?.toList() ?: emptyList()
+ }
+
+ fun tryReadString(resStr: String): String? = tryRead(resStr, ::readString)
+
+ fun readString(resStr: String): String {
+ val resPair = resolveResourceId(resStr)
+ if (resPair == null) {
+ throw IOException("Failed to parse string: $resStr")
+ }
+
+ val (res, id) = resPair
+ return res.getString(id)
+ }
+
+ fun tryReadColor(resStr: String): Int? = tryRead(resStr, ::readColor)
+
+ fun readColor(resStr: String): Int {
+ if (resStr.startsWith("#")) {
+ return Color.parseColor(resStr)
+ }
+
+ val schemeColor = tryParseColorFromScheme(resStr)
+ if (schemeColor != null) {
+ logColor("ColorScheme: $resStr", schemeColor)
+ return checkChroma(schemeColor)
+ }
+
+ val result = resolveColorResourceId(resStr)
+ if (result == null) {
+ throw IOException("Failed to parse color: $resStr")
+ }
+
+ val (res, colorId, targetTone) = result
+ val color = res.getColor(colorId)
+ if (targetTone == null || TonalPalette.SHADE_KEYS.contains(targetTone.toInt())) {
+ logColor("Resources: $resStr", color)
+ return checkChroma(color)
+ } else {
+ val interpolatedColor =
+ ColorStateList.valueOf(color)
+ .withLStar((1000f - targetTone) / 10f)
+ .getDefaultColor()
+ logColor("Resources (interpolated tone): $resStr", interpolatedColor)
+ return checkChroma(interpolatedColor)
+ }
+ }
+
+ private fun checkChroma(color: Int): Int {
+ return overrideChroma?.let {
+ val cam = Cam.fromInt(color)
+ val tone = CamUtils.lstarFromInt(color)
+ val result = ColorUtils.CAMToColor(cam.hue, it, tone)
+ logColor("Chroma override", result)
+ result
+ } ?: color
+ }
+
+ private fun tryParseColorFromScheme(resStr: String): Int? {
+ val colorScheme = this.colorScheme
+ if (colorScheme == null) {
+ logger.w("No color scheme available")
+ return null
+ }
+
+ val (packageName, category, name) = parseResourceId(resStr)
+ if (packageName != "android" || category != "color") {
+ logger.w("Failed to parse package from $resStr")
+ return null
+ }
+
+ var parts = name.split('_')
+ if (parts.size != 3) {
+ logger.w("Failed to find palette and shade from $name")
+ return null
+ }
+ val (_, paletteKey, shadeKeyStr) = parts
+
+ val palette =
+ when (paletteKey) {
+ "accent1" -> colorScheme.accent1
+ "accent2" -> colorScheme.accent2
+ "accent3" -> colorScheme.accent3
+ "neutral1" -> colorScheme.neutral1
+ "neutral2" -> colorScheme.neutral2
+ else -> return null
+ }
+
+ if (shadeKeyStr.contains("+") || shadeKeyStr.contains("-")) {
+ val signIndex = shadeKeyStr.indexOfLast { it == '-' || it == '+' }
+ // Use the tone of the seed color if it was set explicitly.
+ var baseTone =
+ if (seedColor != null) colorScheme.seedTone.toFloat()
+ else shadeKeyStr.substring(0, signIndex).toFloatOrNull()
+ val diff = shadeKeyStr.substring(signIndex).toFloatOrNull()
+
+ if (baseTone == null) {
+ logger.w("Failed to parse base tone from $shadeKeyStr")
+ return null
+ }
+
+ if (diff == null) {
+ logger.w("Failed to parse relative tone from $shadeKeyStr")
+ return null
+ }
+ return palette.getAtTone(baseTone + diff)
+ } else {
+ val shadeKey = shadeKeyStr.toIntOrNull()
+ if (shadeKey == null) {
+ logger.w("Failed to parse tone from $shadeKeyStr")
+ return null
+ }
+ return palette.allShadesMapped.get(shadeKey) ?: palette.getAtTone(shadeKey.toFloat())
+ }
+ }
+
+ fun readFontAsset(resStr: String): Typeface = typefaceCache.getTypeface(resStr)
+
+ fun tryReadTextAsset(path: String?): String? = tryRead(path, ::readTextAsset)
+
+ fun readTextAsset(path: String): String {
+ return pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
+ val buffer = ByteArray(stream.available())
+ stream.read(buffer)
+ String(buffer)
+ }
+ }
+
+ fun tryReadDrawableAsset(path: String?): Drawable? = tryRead(path, ::readDrawableAsset)
+
+ fun readDrawableAsset(path: String): Drawable {
+ var result: Drawable?
+
+ if (path.startsWith("@")) {
+ val pair = resolveResourceId(path)
+ if (pair == null) {
+ throw IOException("Failed to parse $path to an id")
+ }
+ val (res, id) = pair
+ result = res.getDrawable(id)
+ } else if (path.endsWith("xml")) {
+ // TODO(b/248609434): Support xml files in assets
+ throw IOException("Cannot load xml files from assets")
+ } else {
+ // Attempt to load as if it's a bitmap and directly loadable
+ result =
+ pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
+ Drawable.createFromResourceStream(
+ pluginCtx.resources,
+ TypedValue(),
+ stream,
+ null,
+ )
+ }
+ }
+
+ return result ?: throw IOException("Failed to load: $baseDir$path")
+ }
+
+ fun parseResourceId(resStr: String): Triple<String?, String, String> {
+ if (!resStr.startsWith("@")) {
+ throw IOException("Invalid resource id: $resStr; Must start with '@'")
+ }
+
+ // Parse out resource string
+ val parts = resStr.drop(1).split('/', ':')
+ return when (parts.size) {
+ 2 -> Triple(null, parts[0], parts[1])
+ 3 -> Triple(parts[0], parts[1], parts[2])
+ else -> throw IOException("Failed to parse resource string: $resStr")
+ }
+ }
+
+ fun resolveColorResourceId(resStr: String): Triple<Resources, Int, Float?>? {
+ var (packageName, category, name) = parseResourceId(resStr)
+
+ // Convert relative tonal specifiers to standard
+ val relIndex = name.indexOfLast { it == '_' }
+ val isToneRelative = name.contains("-") || name.contains("+")
+ val targetTone =
+ if (packageName != "android") {
+ null
+ } else if (isToneRelative) {
+ val signIndex = name.indexOfLast { it == '-' || it == '+' }
+ val baseTone = name.substring(relIndex + 1, signIndex).toFloatOrNull()
+ var diff = name.substring(signIndex).toFloatOrNull()
+ if (baseTone == null || diff == null) {
+ logger.w("Failed to parse relative tone from $name")
+ return null
+ }
+ baseTone + diff
+ } else {
+ val absTone = name.substring(relIndex + 1).toFloatOrNull()
+ if (absTone == null) {
+ logger.w("Failed to parse absolute tone from $name")
+ return null
+ }
+ absTone
+ }
+
+ if (
+ targetTone != null &&
+ (isToneRelative || !TonalPalette.SHADE_KEYS.contains(targetTone.toInt()))
+ ) {
+ val closeTone = TonalPalette.SHADE_KEYS.minBy { abs(it - targetTone) }
+ val prevName = name
+ name = name.substring(0, relIndex + 1) + closeTone
+ logger.i("Converted $prevName to $name")
+ }
+
+ val result = resolveResourceId(packageName, category, name)
+ if (result == null) {
+ return null
+ }
+
+ val (res, resId) = result
+ return Triple(res, resId, targetTone)
+ }
+
+ fun resolveResourceId(resStr: String): Pair<Resources, Int>? {
+ val (packageName, category, name) = parseResourceId(resStr)
+ return resolveResourceId(packageName, category, name)
+ }
+
+ fun resolveResourceId(
+ packageName: String?,
+ category: String,
+ name: String,
+ ): Pair<Resources, Int>? {
+ for ((res, ctxPkgName) in resources) {
+ val result = res.getIdentifier(name, category, packageName ?: ctxPkgName)
+ if (result != 0) {
+ return Pair(res, result)
+ }
+ }
+ return null
+ }
+
+ private fun <TArg : Any, TRes : Any> tryRead(arg: TArg?, fn: (TArg) -> TRes): TRes? {
+ try {
+ if (arg == null) {
+ return null
+ }
+ return fn(arg)
+ } catch (ex: IOException) {
+ logger.w("Failed to read $arg", ex)
+ return null
+ }
+ }
+
+ fun assetExists(path: String): Boolean {
+ try {
+ if (path.startsWith("@")) {
+ val pair = resolveResourceId(path)
+ val colorPair = resolveColorResourceId(path)
+ return pair != null || colorPair != null
+ } else {
+ val stream = pluginCtx.resources.assets.open("$baseDir$path")
+ if (stream == null) {
+ return false
+ }
+
+ stream.close()
+ return true
+ }
+ } catch (ex: IOException) {
+ return false
+ }
+ }
+
+ fun copy(messageBuffer: MessageBuffer? = null): AssetLoader =
+ AssetLoader(
+ pluginCtx,
+ sysuiCtx,
+ baseDir,
+ colorScheme,
+ seedColor,
+ overrideChroma,
+ typefaceCache,
+ getThemeSeedColor,
+ messageBuffer ?: logger.buffer,
+ )
+
+ fun setSeedColor(seedColor: Int?, style: MonetStyle?) {
+ this.seedColor = seedColor
+ refreshColorPalette(style)
+ }
+
+ fun refreshColorPalette(style: MonetStyle?) {
+ val seedColor =
+ this.seedColor ?: getThemeSeedColor(sysuiCtx).also { logColor("Theme Seed Color", it) }
+ this.colorScheme =
+ ColorScheme(
+ seedColor,
+ false, // darkTheme is not used for palette generation
+ style ?: MonetStyle.CLOCK,
+ )
+
+ // Enforce low chroma on output colors if low chroma theme is selected
+ this.overrideChroma = run {
+ val cam = colorScheme?.seed?.let { Cam.fromInt(it) }
+ if (cam != null && cam.chroma < LOW_CHROMA_LIMIT) {
+ return@run cam.chroma * LOW_CHROMA_SCALE
+ }
+ return@run null
+ }
+ }
+
+ fun getClockPaddingStart(): Int {
+ val result = resolveResourceId(null, "dimen", "clock_padding_start")
+ if (result != null) {
+ val (res, id) = result
+ return res.getDimensionPixelSize(id)
+ }
+ return -1
+ }
+
+ fun getStatusBarHeight(): Int {
+ val display = pluginCtx.getDisplayNoVerify()
+ if (display != null) {
+ return SystemBarUtils.getStatusBarHeight(pluginCtx.resources, display.cutout)
+ }
+
+ logger.w("No display available; falling back to android.R.dimen.status_bar_height")
+ val statusBarHeight = resolveResourceId("android", "dimen", "status_bar_height")
+ if (statusBarHeight != null) {
+ val (res, resId) = statusBarHeight
+ return res.getDimensionPixelSize(resId)
+ }
+
+ throw Exception("Could not fetch StatusBarHeight")
+ }
+
+ fun getResourcesId(name: String): Int = getResource("id", name) { _, id -> id }
+
+ fun getDimen(name: String): Int = getResource("dimen", name, Resources::getDimensionPixelSize)
+
+ fun getString(name: String): String = getResource("string", name, Resources::getString)
+
+ private fun <T> getResource(
+ category: String,
+ name: String,
+ getter: (res: Resources, id: Int) -> T,
+ ): T {
+ val result = resolveResourceId(null, category, name)
+ if (result != null) {
+ val (res, id) = result
+ if (id == -1) throw Exception("Cannot find id of $id from $TAG")
+ return getter(res, id)
+ }
+ throw Exception("Cannot find id of $name from $TAG")
+ }
+
+ private fun logColor(name: String, color: Int) {
+ if (DEBUG_COLOR) {
+ val cam = Cam.fromInt(color)
+ val tone = CamUtils.lstarFromInt(color)
+ logger.i("$name -> (hue: ${cam.hue}, chroma: ${cam.chroma}, tone: $tone)")
+ }
+ }
+
+ companion object {
+ private val DEBUG_COLOR = true
+ private val LOW_CHROMA_LIMIT = 15
+ private val LOW_CHROMA_SCALE = 1.5f
+ private val TAG = AssetLoader::class.simpleName!!
+
+ private fun getThemeSeedColor(ctx: Context): Int {
+ return ctx.resources.getColor(android.R.color.system_palette_key_color_primary_light)
+ }
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt
new file mode 100644
index 000000000000..5a041691b06a
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.shared.clocks
+
+object ClockAnimation {
+ const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
new file mode 100644
index 000000000000..f5e843234095
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
@@ -0,0 +1,288 @@
+/*
+ * 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.shared.clocks
+
+import android.graphics.Point
+import android.view.animation.Interpolator
+import com.android.app.animation.Interpolators
+import com.android.internal.annotations.Keep
+import com.android.systemui.monet.Style as MonetStyle
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
+
+/** Data format for a simple asset-defined clock */
+@Keep
+data class ClockDesign(
+ val id: String,
+ val name: String? = null,
+ val description: String? = null,
+ val thumbnail: String? = null,
+ val large: ClockFace? = null,
+ val small: ClockFace? = null,
+ val colorPalette: MonetStyle? = null,
+)
+
+/** Describes a clock using layers */
+@Keep
+data class ClockFace(
+ val layers: List<ClockLayer> = listOf<ClockLayer>(),
+ val layerBounds: LayerBounds = LayerBounds.FIT,
+ val wallpaper: String? = null,
+ val faceLayout: DigitalFaceLayout? = null,
+ val pickerScale: ClockFaceScaleInPicker? = ClockFaceScaleInPicker(1.0f, 1.0f),
+)
+
+@Keep data class ClockFaceScaleInPicker(val scaleX: Float, val scaleY: Float)
+
+/** Base Type for a Clock Layer */
+@Keep
+interface ClockLayer {
+ /** Override of face LayerBounds setting for this layer */
+ val layerBounds: LayerBounds?
+}
+
+/** Clock layer that renders a static asset */
+@Keep
+data class AssetLayer(
+ /** Asset to render in this layer */
+ val asset: AssetReference,
+ override val layerBounds: LayerBounds? = null,
+) : ClockLayer
+
+/** Clock layer that renders the time (or a component of it) using numerals */
+@Keep
+data class DigitalHandLayer(
+ /** See SimpleDateFormat for timespec format info */
+ val timespec: DigitalTimespec,
+ val style: TextStyle,
+ // adoStyle concrete type must match style,
+ // cause styles will transition between style and aodStyle
+ val aodStyle: TextStyle?,
+ val timer: Int? = null,
+ override val layerBounds: LayerBounds? = null,
+ var faceLayout: DigitalFaceLayout? = null,
+ // we pass 12-hour format from json, which will be converted to 24-hour format in codes
+ val dateTimeFormat: String,
+ val alignment: DigitalAlignment?,
+ // ratio of margins to measured size, currently used for handwritten clocks
+ val marginRatio: DigitalMarginRatio? = DigitalMarginRatio(),
+) : ClockLayer
+
+/** Clock layer that renders the time (or a component of it) using numerals */
+@Keep
+data class ComposedDigitalHandLayer(
+ val customizedView: String? = null,
+ /** See SimpleDateFormat for timespec format info */
+ val digitalLayers: List<DigitalHandLayer> = listOf<DigitalHandLayer>(),
+ override val layerBounds: LayerBounds? = null,
+) : ClockLayer
+
+@Keep
+data class DigitalAlignment(
+ val horizontalAlignment: HorizontalAlignment?,
+ val verticalAlignment: VerticalAlignment?,
+)
+
+@Keep
+data class DigitalMarginRatio(
+ val left: Float = 0F,
+ val top: Float = 0F,
+ val right: Float = 0F,
+ val bottom: Float = 0F,
+)
+
+/** Clock layer which renders a component of the time using an analog hand */
+@Keep
+data class AnalogHandLayer(
+ val timespec: AnalogTimespec,
+ val tickMode: AnalogTickMode,
+ val asset: AssetReference,
+ val timer: Int? = null,
+ val clock_pivot: Point = Point(0, 0),
+ val asset_pivot: Point? = null,
+ val length: Float = 1f,
+ override val layerBounds: LayerBounds? = null,
+) : ClockLayer
+
+/** Clock layer which renders the time using an AVD */
+@Keep
+data class AnimatedHandLayer(
+ val timespec: AnalogTimespec,
+ val asset: AssetReference,
+ val timer: Int? = null,
+ override val layerBounds: LayerBounds? = null,
+) : ClockLayer
+
+/** A collection of asset references for use in different device modes */
+@Keep
+data class AssetReference(
+ val light: String,
+ val dark: String,
+ val doze: String? = null,
+ val lightTint: String? = null,
+ val darkTint: String? = null,
+ val dozeTint: String? = null,
+)
+
+/**
+ * Core TextStyling attributes for text clocks. Both color and sizing information can be applied to
+ * either subtype.
+ */
+@Keep
+interface TextStyle {
+ // fontSizeScale is a scale factor applied to the default clock's font size.
+ val fontSizeScale: Float?
+}
+
+/**
+ * This specifies a font and styling parameters for that font. This is rendered using a text view
+ * and the text animation classes used by the default clock. To ensure default value take effects,
+ * all parameters MUST have a default value
+ */
+@Keep
+data class FontTextStyle(
+ // Font to load and use in the TextView
+ val fontFamily: String? = null,
+ val lineHeight: Float? = null,
+ val borderWidth: String? = null,
+ // ratio of borderWidth / fontSize
+ val borderWidthScale: Float? = null,
+ // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
+ val fillColorLight: String? = null,
+ // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
+ val fillColorDark: String? = null,
+ override val fontSizeScale: Float? = null,
+ /**
+ * use `wdth` for width, `wght` for weight, 'opsz' for optical size single quote for tag name,
+ * and no quote for value separate different axis with `,` e.g. "'wght' 1000, 'wdth' 108, 'opsz'
+ * 90"
+ */
+ var fontVariation: String? = null,
+ // used when alternate in one font file is needed
+ var fontFeatureSettings: String? = null,
+ val renderType: RenderType = RenderType.STROKE_TEXT,
+ val outlineColor: String? = null,
+ val transitionDuration: Long = -1L,
+ val transitionInterpolator: InterpolatorEnum? = null,
+) : TextStyle
+
+/**
+ * As an alternative to using a font, we can instead render a digital clock using a set of drawables
+ * for each numeral, and optionally a colon. These drawables will be rendered directly after sizing
+ * and placing them. This may be easier than generating a font file in some cases, and is provided
+ * for ease of use. Unlike fonts, these are not localizable to other numeric systems (like Burmese).
+ */
+@Keep
+data class LottieTextStyle(
+ val numbers: List<String> = listOf(),
+ // Spacing between numbers, dimension string
+ val spacing: String = "0dp",
+ // Colon drawable may be omitted if unused in format spec
+ val colon: String? = null,
+ // key is keypath name to get strokes from lottie, value is the color name to query color in
+ // palette, e.g. @android:color/system_accent1_100
+ val fillColorLightMap: Map<String, String>? = null,
+ val fillColorDarkMap: Map<String, String>? = null,
+ override val fontSizeScale: Float? = null,
+ val paddingVertical: String = "0dp",
+ val paddingHorizontal: String = "0dp",
+) : TextStyle
+
+/** Layer sizing mode for the clockface or layer */
+enum class LayerBounds {
+ /**
+ * Sized so the larger dimension matches the allocated space. This results in some of the
+ * allocated space being unused.
+ */
+ FIT,
+
+ /**
+ * Sized so the smaller dimension matches the allocated space. This will clip some content to
+ * the edges of the space.
+ */
+ FILL,
+
+ /** Fills the allocated space exactly by stretching the layer */
+ STRETCH,
+}
+
+/** Ticking mode for analog hands. */
+enum class AnalogTickMode {
+ SWEEP,
+ TICK,
+}
+
+/** Timspec options for Analog Hands. Named for tick interval. */
+enum class AnalogTimespec {
+ SECONDS,
+ MINUTES,
+ HOURS,
+ HOURS_OF_DAY,
+ DAY_OF_WEEK,
+ DAY_OF_MONTH,
+ DAY_OF_YEAR,
+ WEEK,
+ MONTH,
+ TIMER,
+}
+
+enum class DigitalTimespec {
+ TIME_FULL_FORMAT,
+ DIGIT_PAIR,
+ FIRST_DIGIT,
+ SECOND_DIGIT,
+ DATE_FORMAT,
+}
+
+enum class DigitalFaceLayout {
+ // can only use HH_PAIR, MM_PAIR from DigitalTimespec
+ TWO_PAIRS_VERTICAL,
+ TWO_PAIRS_HORIZONTAL,
+ // can only use HOUR_FIRST_DIGIT, HOUR_SECOND_DIGIT, MINUTE_FIRST_DIGIT, MINUTE_SECOND_DIGIT
+ // from DigitalTimespec, used for tabular layout when the font doesn't support tnum
+ FOUR_DIGITS_ALIGN_CENTER,
+ FOUR_DIGITS_HORIZONTAL,
+}
+
+enum class RenderType {
+ CHANGE_WEIGHT,
+ HOLLOW_TEXT,
+ STROKE_TEXT,
+ OUTER_OUTLINE_TEXT,
+}
+
+enum class InterpolatorEnum(factory: () -> Interpolator) {
+ STANDARD({ Interpolators.STANDARD }),
+ EMPHASIZED({ Interpolators.EMPHASIZED });
+
+ val interpolator: Interpolator by lazy(factory)
+}
+
+fun generateDigitalLayerIdString(layer: DigitalHandLayer): String {
+ return if (
+ layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
+ layer.timespec == DigitalTimespec.DATE_FORMAT
+ ) {
+ layer.timespec.toString()
+ } else {
+ if ("h" in layer.dateTimeFormat) {
+ "HOUR" + "_" + layer.timespec.toString()
+ } else {
+ "MINUTE" + "_" + layer.timespec.toString()
+ }
+ }
+}
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 954155d16b05..9da3022fc0d8 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
@@ -65,7 +65,7 @@ private val KNOWN_PLUGINS =
private fun <TKey : Any, TVal : Any> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut(
key: TKey,
value: TVal,
- onNew: (TVal) -> Unit
+ onNew: (TVal) -> Unit,
): TVal {
val result = this.putIfAbsent(key, value)
if (result == null) {
@@ -110,7 +110,7 @@ open class ClockRegistry(
selfChange: Boolean,
uris: Collection<Uri>,
flags: Int,
- userId: Int
+ userId: Int,
) {
scope.launch(bgDispatcher) { querySettings() }
}
@@ -180,7 +180,7 @@ open class ClockRegistry(
override fun onPluginLoaded(
plugin: ClockProviderPlugin,
pluginContext: Context,
- manager: PluginLifecycleManager<ClockProviderPlugin>
+ manager: PluginLifecycleManager<ClockProviderPlugin>,
) {
plugin.initialize(clockBuffers)
@@ -218,7 +218,7 @@ open class ClockRegistry(
override fun onPluginUnloaded(
plugin: ClockProviderPlugin,
- manager: PluginLifecycleManager<ClockProviderPlugin>
+ manager: PluginLifecycleManager<ClockProviderPlugin>,
) {
for (clock in plugin.getClocks()) {
val id = clock.clockId
@@ -290,12 +290,12 @@ open class ClockRegistry(
Settings.Secure.getStringForUser(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
- ActivityManager.getCurrentUser()
+ ActivityManager.getCurrentUser(),
)
} else {
Settings.Secure.getString(
context.contentResolver,
- Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
+ Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
)
}
@@ -320,13 +320,13 @@ open class ClockRegistry(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
json,
- ActivityManager.getCurrentUser()
+ ActivityManager.getCurrentUser(),
)
} else {
Settings.Secure.putString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
- json
+ json,
)
}
} catch (ex: Exception) {
@@ -418,7 +418,7 @@ open class ClockRegistry(
pluginManager.addPluginListener(
pluginListener,
ClockProviderPlugin::class.java,
- /*allowMultiple=*/ true
+ /*allowMultiple=*/ true,
)
scope.launch(bgDispatcher) { querySettings() }
@@ -427,7 +427,7 @@ open class ClockRegistry(
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
/*notifyForDescendants=*/ false,
settingObserver,
- UserHandle.USER_ALL
+ UserHandle.USER_ALL,
)
ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG)
@@ -435,7 +435,7 @@ open class ClockRegistry(
context.contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
/*notifyForDescendants=*/ false,
- settingObserver
+ settingObserver,
)
}
}
@@ -504,7 +504,7 @@ open class ClockRegistry(
val isCurrent = currentClockId == info.metadata.clockId
logger.log(
if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
- { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+ { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
) {
str1 = info.metadata.clockId
str2 = info.manager.toString()
@@ -516,7 +516,7 @@ open class ClockRegistry(
val isCurrent = currentClockId == info.metadata.clockId
logger.log(
if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
- { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+ { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
) {
str1 = info.metadata.clockId
str2 = info.manager.toString()
@@ -532,7 +532,7 @@ open class ClockRegistry(
val isCurrent = currentClockId == info.metadata.clockId
logger.log(
if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG,
- { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+ { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
) {
str1 = info.metadata.clockId
str2 = info.manager.toString()
@@ -548,7 +548,7 @@ open class ClockRegistry(
val isCurrent = currentClockId == info.metadata.clockId
logger.log(
if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
- { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+ { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
) {
str1 = info.metadata.clockId
str2 = info.manager.toString()
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 4802e3447eb2..07191c671a34 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
@@ -34,7 +34,7 @@ class DefaultClockProvider(
val layoutInflater: LayoutInflater,
val resources: Resources,
val hasStepClockAnimation: Boolean = false,
- val migratedClocks: Boolean = false
+ val migratedClocks: Boolean = false,
) : ClockProvider {
private var messageBuffers: ClockMessageBuffers? = null
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
new file mode 100644
index 000000000000..38697063bea5
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.shared.clocks
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.TimeInterpolator
+import android.animation.ValueAnimator
+import android.graphics.Point
+
+class DigitTranslateAnimator(val updateCallback: () -> Unit) {
+ val DEFAULT_ANIMATION_DURATION = 500L
+ val updatedTranslate = Point(0, 0)
+
+ val baseTranslation = Point(0, 0)
+ var targetTranslation: Point? = null
+ val bounceAnimator: ValueAnimator =
+ ValueAnimator.ofFloat(1f).apply {
+ duration = DEFAULT_ANIMATION_DURATION
+ addUpdateListener {
+ updateTranslation(it.animatedFraction, updatedTranslate)
+ updateCallback()
+ }
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ rebase()
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ rebase()
+ }
+ }
+ )
+ }
+
+ fun rebase() {
+ baseTranslation.x = updatedTranslate.x
+ baseTranslation.y = updatedTranslate.y
+ }
+
+ fun animatePosition(
+ animate: Boolean = true,
+ delay: Long = 0,
+ duration: Long = -1L,
+ interpolator: TimeInterpolator? = null,
+ targetTranslation: Point? = null,
+ onAnimationEnd: Runnable? = null,
+ ) {
+ this.targetTranslation = targetTranslation ?: Point(0, 0)
+ if (animate) {
+ bounceAnimator.cancel()
+ bounceAnimator.startDelay = delay
+ bounceAnimator.duration =
+ if (duration == -1L) {
+ DEFAULT_ANIMATION_DURATION
+ } else {
+ duration
+ }
+ interpolator?.let { bounceAnimator.interpolator = it }
+ if (onAnimationEnd != null) {
+ val listener =
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ onAnimationEnd.run()
+ bounceAnimator.removeListener(this)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ bounceAnimator.removeListener(this)
+ }
+ }
+ bounceAnimator.addListener(listener)
+ }
+ bounceAnimator.start()
+ } else {
+ // No animation is requested, thus set base and target state to the same state.
+ updateTranslation(1F, updatedTranslate)
+ rebase()
+ updateCallback()
+ }
+ }
+
+ fun updateTranslation(progress: Float, outPoint: Point) {
+ outPoint.x =
+ (baseTranslation.x + progress * (targetTranslation!!.x - baseTranslation.x)).toInt()
+ outPoint.y =
+ (baseTranslation.y + progress * (targetTranslation!!.y - baseTranslation.y)).toInt()
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt
new file mode 100644
index 000000000000..2be6c6573ebe
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.util.TypedValue
+import java.util.regex.Pattern
+
+class DimensionParser(private val ctx: Context) {
+ fun convert(dimension: String?): Float? {
+ if (dimension == null) {
+ return null
+ }
+ return convert(dimension)
+ }
+
+ fun convert(dimension: String): Float {
+ val metrics = ctx.resources.displayMetrics
+ val (value, unit) = parse(dimension)
+ return TypedValue.applyDimension(unit, value, metrics)
+ }
+
+ fun parse(dimension: String): Pair<Float, Int> {
+ val matcher = parserPattern.matcher(dimension)
+ if (!matcher.matches()) {
+ throw NumberFormatException("Failed to parse '$dimension'")
+ }
+
+ val value =
+ matcher.group(1)?.toFloat() ?: throw NumberFormatException("Bad value in '$dimension'")
+ val unit =
+ dimensionMap.get(matcher.group(3) ?: "")
+ ?: throw NumberFormatException("Bad unit in '$dimension'")
+ return Pair(value, unit)
+ }
+
+ private companion object {
+ val parserPattern = Pattern.compile("(\\d+(\\.\\d+)?)([a-z]+)")
+ val dimensionMap =
+ mapOf(
+ "dp" to TypedValue.COMPLEX_UNIT_DIP,
+ "dip" to TypedValue.COMPLEX_UNIT_DIP,
+ "sp" to TypedValue.COMPLEX_UNIT_SP,
+ "px" to TypedValue.COMPLEX_UNIT_PX,
+ "pt" to TypedValue.COMPLEX_UNIT_PT,
+ "mm" to TypedValue.COMPLEX_UNIT_MM,
+ "in" to TypedValue.COMPLEX_UNIT_IN,
+ )
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt
new file mode 100644
index 000000000000..34cb4ef7089d
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
+import com.android.systemui.log.core.Logger
+
+object LogUtil {
+ // Used when MessageBuffers are not provided by the host application
+ val DEFAULT_MESSAGE_BUFFER = LogcatOnlyMessageBuffer(LogLevel.INFO)
+
+ // Only intended for use during initialization steps where the correct logger doesn't exist yet
+ val FALLBACK_INIT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.ERROR), "CLOCK_INIT")
+
+ // Debug is primarially used for tests, but can also be used for tracking down hard issues.
+ val DEBUG_MESSAGE_BUFFER = LogcatOnlyMessageBuffer(LogLevel.DEBUG)
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt
new file mode 100644
index 000000000000..f5a9375122b5
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.shared.clocks
+
+import android.graphics.Typeface
+import com.android.systemui.animation.TypefaceVariantCache
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import java.lang.ref.ReferenceQueue
+import java.lang.ref.WeakReference
+
+class TypefaceCache(messageBuffer: MessageBuffer, val typefaceFactory: (String) -> Typeface) {
+ private val logger = Logger(messageBuffer, this::class.simpleName!!)
+
+ private data class CacheKey(val res: String, val fvar: String?)
+
+ private inner class WeakTypefaceRef(val key: CacheKey, typeface: Typeface) :
+ WeakReference<Typeface>(typeface, queue)
+
+ private var totalHits = 0
+
+ private var totalMisses = 0
+
+ private var totalEvictions = 0
+
+ // We use a map of WeakRefs here instead of an LruCache. This prevents needing to resize the
+ // cache depending on the number of distinct fonts used by a clock, as different clocks have
+ // different numbers of simultaneously loaded and configured fonts. Because our clocks tend to
+ // initialize a number of parallel views and animators, our usages of Typefaces overlap. As a
+ // result, once a typeface is no longer being used, it is unlikely to be recreated immediately.
+ private val cache = mutableMapOf<CacheKey, WeakTypefaceRef>()
+ private val queue = ReferenceQueue<Typeface>()
+
+ fun getTypeface(res: String): Typeface {
+ checkQueue()
+ val key = CacheKey(res, null)
+ cache.get(key)?.get()?.let {
+ logHit(key)
+ return it
+ }
+
+ logMiss(key)
+ val result = typefaceFactory(res)
+ cache.put(key, WeakTypefaceRef(key, result))
+ return result
+ }
+
+ fun getVariantCache(res: String): TypefaceVariantCache {
+ val baseTypeface = getTypeface(res)
+ return object : TypefaceVariantCache {
+ override fun getTypefaceForVariant(fvar: String?): Typeface? {
+ checkQueue()
+ val key = CacheKey(res, fvar)
+ cache.get(key)?.get()?.let {
+ logHit(key)
+ return it
+ }
+
+ logMiss(key)
+ return TypefaceVariantCache.createVariantTypeface(baseTypeface, fvar).also {
+ cache.put(key, WeakTypefaceRef(key, it))
+ }
+ }
+ }
+ }
+
+ private fun logHit(key: CacheKey) {
+ totalHits++
+ if (DEBUG_HITS)
+ logger.i({ "HIT: $str1; Total: $int1" }) {
+ str1 = key.toString()
+ int1 = totalHits
+ }
+ }
+
+ private fun logMiss(key: CacheKey) {
+ totalMisses++
+ logger.w({ "MISS: $str1; Total: $int1" }) {
+ str1 = key.toString()
+ int1 = totalMisses
+ }
+ }
+
+ private fun logEviction(key: CacheKey) {
+ totalEvictions++
+ logger.i({ "EVICTED: $str1; Total: $int1" }) {
+ str1 = key.toString()
+ int1 = totalEvictions
+ }
+ }
+
+ private fun checkQueue() =
+ generateSequence { queue.poll() }
+ .filterIsInstance<WeakTypefaceRef>()
+ .forEach {
+ logEviction(it.key)
+ cache.remove(it.key)
+ }
+
+ companion object {
+ private val DEBUG_HITS = false
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
new file mode 100644
index 000000000000..eb7234646a64
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.shared.clocks.view
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Point
+import android.view.View
+import android.widget.FrameLayout
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.LogUtil
+import java.util.Locale
+
+abstract class DigitalClockFaceView(ctx: Context, messageBuffer: MessageBuffer) : FrameLayout(ctx) {
+ protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+ get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
+
+ abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView>
+
+ @VisibleForTesting
+ var isAnimationEnabled = true
+ set(value) {
+ field = value
+ digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value }
+ }
+
+ var dozeFraction: Float = 0F
+ set(value) {
+ field = value
+ digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field }
+ }
+
+ val dozeControlState = DozeControlState()
+
+ var isReactiveTouchInteractionEnabled = false
+ set(value) {
+ field = value
+ }
+
+ open val text: String?
+ get() = null
+
+ open fun refreshTime() = logger.d("refreshTime()")
+
+ override fun invalidate() {
+ logger.d("invalidate()")
+ super.invalidate()
+ }
+
+ override fun requestLayout() {
+ logger.d("requestLayout()")
+ super.requestLayout()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ logger.d("onMeasure()")
+ calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { setMeasuredDimension(it.x, it.y) }
+ ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) }
+ calculateLeftTopPosition()
+ dozeControlState.animateReady = true
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ logger.d("onLayout()")
+ super.onLayout(changed, left, top, right, bottom)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ text?.let { logger.d({ "onDraw($str1)" }) { str1 = it } } ?: run { logger.d("onDraw()") }
+ super.onDraw(canvas)
+ }
+
+ /*
+ * Called in onMeasure to generate width/height overrides to the normal measuring logic. A null
+ * result causes the normal view measuring logic to execute.
+ */
+ protected open fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? = null
+
+ protected open fun calculateLeftTopPosition() {}
+
+ override fun addView(child: View?) {
+ if (child == null) return
+ logger.d({ "addView($str1 @$int1)" }) {
+ str1 = child::class.simpleName!!
+ int1 = child.id
+ }
+ super.addView(child)
+ if (child is SimpleDigitalClockTextView) {
+ digitalClockTextViewMap[child.id] = child
+ }
+ child.setWillNotDraw(true)
+ }
+
+ open fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) }
+ }
+
+ open fun animateCharge() {
+ digitalClockTextViewMap.forEach { _, view -> view.animateCharge() }
+ }
+
+ open fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
+
+ fun updateColors(assets: AssetLoader, isRegionDark: Boolean) {
+ digitalClockTextViewMap.forEach { _, view -> view.updateColors(assets, isRegionDark) }
+ invalidate()
+ }
+
+ fun onFontSettingChanged(fontSizePx: Float) {
+ digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
+ }
+
+ open val hasCustomWeatherDataDisplay
+ get() = false
+
+ open val hasCustomPositionUpdatedAnimation
+ get() = false
+
+ /** True if it's large weather clock, will use weatherBlueprint in compose */
+ open val useCustomClockScene
+ get() = false
+
+ // TODO: implement ClockEventUnion?
+ open fun onLocaleChanged(locale: Locale) {}
+
+ open fun onWeatherDataChanged(data: WeatherData) {}
+
+ open fun onAlarmDataChanged(data: AlarmData) {}
+
+ open fun onZenDataChanged(data: ZenData) {}
+
+ open fun onPickerCarouselSwiping(swipingFraction: Float) {}
+
+ open fun isAlignedWithScreen(): Boolean = false
+
+ /**
+ * animateDoze needs correct translate value, which is calculated in onMeasure so we need to
+ * delay this animation when we get correct values
+ */
+ class DozeControlState {
+ var animateDoze: () -> Unit = {}
+ set(value) {
+ if (animateReady) {
+ value()
+ field = {}
+ } else {
+ field = value
+ }
+ }
+
+ var animateReady = false
+ set(value) {
+ if (value) {
+ animateDoze()
+ animateDoze = {}
+ }
+ field = value
+ }
+ }
+}
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
new file mode 100644
index 000000000000..c29c8dac8ba6
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -0,0 +1,260 @@
+/*
+ * 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.shared.clocks.view
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Point
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import com.android.app.animation.Interpolators
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.DigitTranslateAnimator
+import com.android.systemui.shared.clocks.FontTextStyle
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
+
+class FlexClockView(context: Context, val assetLoader: AssetLoader, messageBuffer: MessageBuffer) :
+ DigitalClockFaceView(context, messageBuffer) {
+ override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
+ val digitLeftTopMap = mutableMapOf<Int, Point>()
+ var maxSingleDigitHeight = -1
+ var maxSingleDigitWidth = -1
+ val lockscreenTranslate = Point(0, 0)
+ val aodTranslate = Point(0, 0)
+
+ init {
+ setWillNotDraw(false)
+ layoutParams =
+ RelativeLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ )
+ }
+
+ private var prevX = 0f
+ private var prevY = 0f
+ private var isDown = false
+
+ // TODO(b/340253296): Genericize; json spec
+ private var wght = 603f
+ private var wdth = 100f
+
+ // TODO(b/340253296): Json spec
+ private val MAX_WGHT = 950f
+ private val MIN_WGHT = 50f
+ private val WGHT_SCALE = 0.5f
+
+ private val MAX_WDTH = 150f
+ private val MIN_WDTH = 0f
+ private val WDTH_SCALE = 0.2f
+
+ override fun onTouchEvent(evt: MotionEvent): Boolean {
+ // TODO(b/340253296): implement on DigitalClockFaceView?
+ if (!isReactiveTouchInteractionEnabled) {
+ return super.onTouchEvent(evt)
+ }
+
+ when (evt.action) {
+ MotionEvent.ACTION_DOWN -> {
+ isDown = true
+ prevX = evt.x
+ prevY = evt.y
+ return true
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ if (!isDown) {
+ return super.onTouchEvent(evt)
+ }
+
+ wdth = clamp(wdth + (evt.x - prevX) * WDTH_SCALE, MIN_WDTH, MAX_WDTH)
+ wght = clamp(wght + (evt.y - prevY) * WGHT_SCALE, MIN_WGHT, MAX_WGHT)
+ prevX = evt.x
+ prevY = evt.y
+
+ // TODO(b/340253296): Genericize; json spec
+ val fvar = "'wght' $wght, 'wdth' $wdth, 'opsz' 144, 'ROND' 100"
+ digitalClockTextViewMap.forEach { (_, view) ->
+ val textStyle = view.textStyle as FontTextStyle
+ textStyle.fontVariation = fvar
+ view.applyStyles(assetLoader, textStyle, view.aodStyle)
+ }
+
+ requestLayout()
+ invalidate()
+ return true
+ }
+
+ MotionEvent.ACTION_UP -> {
+ isDown = false
+ return true
+ }
+ }
+
+ return super.onTouchEvent(evt)
+ }
+
+ override fun addView(child: View?) {
+ super.addView(child)
+ (child as SimpleDigitalClockTextView).digitTranslateAnimator =
+ DigitTranslateAnimator(::invalidate)
+ }
+
+ protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point {
+ digitalClockTextViewMap.forEach { (_, textView) ->
+ textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+ }
+ val textView = digitalClockTextViewMap[R.id.HOUR_FIRST_DIGIT]!!
+ maxSingleDigitHeight = textView.measuredHeight
+ maxSingleDigitWidth = textView.measuredWidth
+ aodTranslate.x = -(maxSingleDigitWidth * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt()
+ aodTranslate.y = (maxSingleDigitHeight * AOD_VERTICAL_TRANSLATE_RATIO).toInt()
+ return Point(
+ ((maxSingleDigitWidth + abs(aodTranslate.x)) * 2),
+ ((maxSingleDigitHeight + abs(aodTranslate.y)) * 2),
+ )
+ }
+
+ protected override fun calculateLeftTopPosition() {
+ digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0)
+ digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitWidth, 0)
+ digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitHeight)
+ digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitWidth, maxSingleDigitHeight)
+ digitLeftTopMap.forEach { _, point ->
+ point.x += abs(aodTranslate.x)
+ point.y += abs(aodTranslate.y)
+ }
+ }
+
+ override fun refreshTime() {
+ super.refreshTime()
+ digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ digitalClockTextViewMap.forEach { (id, _) ->
+ val textView = digitalClockTextViewMap[id]!!
+ canvas.translate(digitLeftTopMap[id]!!.x.toFloat(), digitLeftTopMap[id]!!.y.toFloat())
+ textView.draw(canvas)
+ canvas.translate(-digitLeftTopMap[id]!!.x.toFloat(), -digitLeftTopMap[id]!!.y.toFloat())
+ }
+ }
+
+ override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ dozeControlState.animateDoze = {
+ super.animateDoze(isDozing, isAnimated)
+ if (maxSingleDigitHeight == -1) {
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+ }
+ digitalClockTextViewMap.forEach { (id, textView) ->
+ textView.digitTranslateAnimator?.let {
+ if (!isDozing) {
+ it.animatePosition(
+ animate = isAnimated && isAnimationEnabled,
+ interpolator = Interpolators.EMPHASIZED,
+ duration = AOD_TRANSITION_DURATION,
+ targetTranslation =
+ updateDirectionalTargetTranslate(id, lockscreenTranslate),
+ )
+ } else {
+ it.animatePosition(
+ animate = isAnimated && isAnimationEnabled,
+ interpolator = Interpolators.EMPHASIZED,
+ duration = AOD_TRANSITION_DURATION,
+ onAnimationEnd = null,
+ targetTranslation = updateDirectionalTargetTranslate(id, aodTranslate),
+ )
+ }
+ }
+ }
+ }
+ }
+
+ override fun animateCharge() {
+ super.animateCharge()
+ digitalClockTextViewMap.forEach { (id, textView) ->
+ textView.digitTranslateAnimator?.let {
+ it.animatePosition(
+ animate = isAnimationEnabled,
+ interpolator = Interpolators.EMPHASIZED,
+ duration = CHARGING_TRANSITION_DURATION,
+ onAnimationEnd = {
+ it.animatePosition(
+ animate = isAnimationEnabled,
+ interpolator = Interpolators.EMPHASIZED,
+ duration = CHARGING_TRANSITION_DURATION,
+ targetTranslation =
+ updateDirectionalTargetTranslate(
+ id,
+ if (dozeFraction == 1F) aodTranslate else lockscreenTranslate,
+ ),
+ )
+ },
+ targetTranslation =
+ updateDirectionalTargetTranslate(
+ id,
+ if (dozeFraction == 1F) lockscreenTranslate else aodTranslate,
+ ),
+ )
+ }
+ }
+ }
+
+ companion object {
+ val AOD_TRANSITION_DURATION = 750L
+ val CHARGING_TRANSITION_DURATION = 300L
+
+ val AOD_HORIZONTAL_TRANSLATE_RATIO = 0.15F
+ val AOD_VERTICAL_TRANSLATE_RATIO = 0.075F
+
+ // Use the sign of targetTranslation to control the direction of digit translation
+ fun updateDirectionalTargetTranslate(id: Int, targetTranslation: Point): Point {
+ val outPoint = Point(targetTranslation)
+ when (id) {
+ R.id.HOUR_FIRST_DIGIT -> {
+ outPoint.x *= -1
+ outPoint.y *= -1
+ }
+
+ R.id.HOUR_SECOND_DIGIT -> {
+ outPoint.x *= 1
+ outPoint.y *= -1
+ }
+
+ R.id.MINUTE_FIRST_DIGIT -> {
+ outPoint.x *= -1
+ outPoint.y *= 1
+ }
+
+ R.id.MINUTE_SECOND_DIGIT -> {
+ 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
new file mode 100644
index 000000000000..74617b1c0c5c
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -0,0 +1,654 @@
+/*
+ * 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.shared.clocks.view
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Point
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.text.Layout
+import android.text.TextPaint
+import android.util.AttributeSet
+import android.util.Log
+import android.util.MathUtils
+import android.util.TypedValue
+import android.view.View.MeasureSpec.AT_MOST
+import android.view.View.MeasureSpec.EXACTLY
+import android.view.animation.Interpolator
+import android.widget.TextView
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.animation.TextAnimator
+import com.android.systemui.animation.TypefaceVariantCache
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.ClockAnimation
+import com.android.systemui.shared.clocks.DigitTranslateAnimator
+import com.android.systemui.shared.clocks.DimensionParser
+import com.android.systemui.shared.clocks.FontTextStyle
+import com.android.systemui.shared.clocks.LogUtil
+import com.android.systemui.shared.clocks.RenderType
+import com.android.systemui.shared.clocks.TextStyle
+import java.lang.Thread
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.math.min
+
+private val TAG = SimpleDigitalClockTextView::class.simpleName!!
+
+@SuppressLint("AppCompatCustomView")
+open class SimpleDigitalClockTextView(
+ ctx: Context,
+ messageBuffer: MessageBuffer,
+ attrs: AttributeSet? = null,
+) : TextView(ctx, attrs), SimpleDigitalClockView {
+ val lockScreenPaint = TextPaint()
+ override lateinit var textStyle: FontTextStyle
+ lateinit var aodStyle: FontTextStyle
+ private val parser = DimensionParser(ctx)
+ var maxSingleDigitHeight = -1
+ var maxSingleDigitWidth = -1
+ var digitTranslateAnimator: DigitTranslateAnimator? = null
+ var aodFontSizePx: Float = -1F
+ var isVertical: Boolean = false
+
+ // Store the font size when there's no height constraint as a reference when adjusting font size
+ private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
+ // Calculated by height of styled text view / text size
+ // Used as a factor to calculate a smaller font size when text height is constrained
+ @VisibleForTesting var fontSizeAdjustFactor = 1F
+
+ private val initThread = Thread.currentThread()
+
+ // textBounds is the size of text in LS, which only measures current text in lockscreen style
+ var textBounds = Rect()
+ // prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD
+ // especially for the textView which has different bounds during the animation
+ // prevTextBounds holds the state we are transitioning from
+ private val prevTextBounds = Rect()
+ // targetTextBounds holds the state we are interpolating to
+ private val targetTextBounds = Rect()
+ protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+ get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
+
+ private var aodDozingInterpolator: Interpolator? = null
+
+ @VisibleForTesting lateinit var textAnimator: TextAnimator
+ @VisibleForTesting var outlineAnimator: TextAnimator? = null
+ // used for hollow style for AOD version
+ // because stroke style for some fonts have some unwanted inner strokes
+ // we want to draw this layer on top to oclude them
+ @VisibleForTesting var innerAnimator: TextAnimator? = null
+
+ lateinit var typefaceCache: TypefaceVariantCache
+ private set
+
+ private fun setTypefaceCache(value: TypefaceVariantCache) {
+ typefaceCache = value
+ if (this::textAnimator.isInitialized) {
+ textAnimator.typefaceCache = value
+ }
+ outlineAnimator?.typefaceCache = value
+ innerAnimator?.typefaceCache = value
+ }
+
+ @VisibleForTesting
+ var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
+ TextAnimator(layout, ClockAnimation.NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb).also {
+ if (this::typefaceCache.isInitialized) {
+ it.typefaceCache = typefaceCache
+ }
+ }
+ }
+
+ override var verticalAlignment: VerticalAlignment = VerticalAlignment.CENTER
+ override var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
+ override var isAnimationEnabled = true
+ override var dozeFraction: Float = 0F
+ set(value) {
+ field = value
+ invalidate()
+ }
+
+ // Have to passthrough to unify View with SimpleDigitalClockView
+ override var text: String
+ get() = super.getText().toString()
+ set(value) = super.setText(value)
+
+ var textBorderWidth = 0F
+ var aodBorderWidth = 0F
+ var baselineFromMeasure = 0
+
+ var textFillColor: Int? = null
+ var textOutlineColor = TEXT_OUTLINE_DEFAULT_COLOR
+ var aodFillColor = AOD_DEFAULT_COLOR
+ var aodOutlineColor = AOD_OUTLINE_DEFAULT_COLOR
+
+ override fun updateColors(assets: AssetLoader, isRegionDark: Boolean) {
+ val fillColor = if (isRegionDark) textStyle.fillColorLight else textStyle.fillColorDark
+ textFillColor =
+ fillColor?.let { assets.readColor(it) }
+ ?: assets.seedColor
+ ?: getDefaultColor(assets, isRegionDark)
+ // for NumberOverlapView to read correct color
+ lockScreenPaint.color = textFillColor as Int
+ textStyle.outlineColor?.let { textOutlineColor = assets.readColor(it) }
+ ?: run { textOutlineColor = TEXT_OUTLINE_DEFAULT_COLOR }
+ (aodStyle.fillColorLight ?: aodStyle.fillColorDark)?.let {
+ aodFillColor = assets.readColor(it)
+ } ?: run { aodFillColor = AOD_DEFAULT_COLOR }
+ aodStyle.outlineColor?.let { aodOutlineColor = assets.readColor(it) }
+ ?: run { aodOutlineColor = AOD_OUTLINE_DEFAULT_COLOR }
+ if (dozeFraction < 1f) {
+ textAnimator.setTextStyle(color = textFillColor, animate = false)
+ outlineAnimator?.setTextStyle(color = textOutlineColor, animate = false)
+ }
+ invalidate()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ logger.d("onMeasure()")
+ if (isVertical) {
+ // use at_most to avoid apply measuredWidth from last measuring to measuredHeight
+ // cause we use max to setMeasuredDimension
+ super.onMeasure(
+ MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), AT_MOST),
+ MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), AT_MOST),
+ )
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+
+ val layout = this.layout
+ if (layout != null) {
+ if (!this::textAnimator.isInitialized) {
+ textAnimator = textAnimatorFactory(layout, ::invalidate)
+ outlineAnimator = textAnimatorFactory(layout) {}
+ innerAnimator = textAnimatorFactory(layout) {}
+ setInterpolatorPaint()
+ } else {
+ textAnimator.updateLayout(layout)
+ outlineAnimator?.updateLayout(layout)
+ innerAnimator?.updateLayout(layout)
+ }
+ baselineFromMeasure = layout.getLineBaseline(0)
+ } else {
+ val currentThread = Thread.currentThread()
+ Log.wtf(
+ TAG,
+ "TextView.getLayout() is null after measure! " +
+ "currentThread=$currentThread; initThread=$initThread",
+ )
+ }
+
+ var expectedWidth: Int
+ var expectedHeight: Int
+
+ if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+ // For view which has fixed height, e.g. small clock,
+ // we should always return the size required from parent view
+ expectedHeight = heightMeasureSpec
+ } else {
+ expectedHeight =
+ MeasureSpec.makeMeasureSpec(
+ if (isSingleDigit()) {
+ maxSingleDigitHeight
+ } else {
+ textBounds.height() + 2 * lockScreenPaint.strokeWidth.toInt()
+ },
+ MeasureSpec.getMode(measuredHeight),
+ )
+ }
+ if (MeasureSpec.getMode(widthMeasureSpec) == EXACTLY) {
+ expectedWidth = widthMeasureSpec
+ } else {
+ expectedWidth =
+ MeasureSpec.makeMeasureSpec(
+ if (isSingleDigit()) {
+ maxSingleDigitWidth
+ } else {
+ max(
+ textBounds.width() + 2 * lockScreenPaint.strokeWidth.toInt(),
+ MeasureSpec.getSize(measuredWidth),
+ )
+ },
+ MeasureSpec.getMode(measuredWidth),
+ )
+ }
+
+ if (isVertical) {
+ expectedWidth = expectedHeight.also { expectedHeight = expectedWidth }
+ }
+ setMeasuredDimension(expectedWidth, expectedHeight)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ if (isVertical) {
+ canvas.save()
+ canvas.translate(0F, measuredHeight.toFloat())
+ canvas.rotate(-90F)
+ }
+ logger.d({ "onDraw(); ls: $str1; aod: $str2;" }) {
+ str1 = textAnimator.textInterpolator.shapedText
+ str2 = outlineAnimator?.textInterpolator?.shapedText
+ }
+ val translation = getLocalTranslation()
+ canvas.translate(translation.x.toFloat(), translation.y.toFloat())
+ digitTranslateAnimator?.let {
+ canvas.translate(it.updatedTranslate.x.toFloat(), it.updatedTranslate.y.toFloat())
+ }
+
+ if (aodStyle.renderType == RenderType.HOLLOW_TEXT) {
+ canvas.saveLayer(
+ -translation.x.toFloat(),
+ -translation.y.toFloat(),
+ (-translation.x + measuredWidth).toFloat(),
+ (-translation.y + measuredHeight).toFloat(),
+ null,
+ )
+ outlineAnimator?.draw(canvas)
+ canvas.saveLayer(
+ -translation.x.toFloat(),
+ -translation.y.toFloat(),
+ (-translation.x + measuredWidth).toFloat(),
+ (-translation.y + measuredHeight).toFloat(),
+ Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) },
+ )
+ innerAnimator?.draw(canvas)
+ canvas.restore()
+ canvas.restore()
+ } else if (aodStyle.renderType != RenderType.CHANGE_WEIGHT) {
+ outlineAnimator?.draw(canvas)
+ }
+ textAnimator.draw(canvas)
+
+ digitTranslateAnimator?.let {
+ canvas.translate(-it.updatedTranslate.x.toFloat(), -it.updatedTranslate.y.toFloat())
+ }
+ canvas.translate(-translation.x.toFloat(), -translation.y.toFloat())
+ if (isVertical) {
+ canvas.restore()
+ }
+ }
+
+ override fun invalidate() {
+ logger.d("invalidate()")
+ super.invalidate()
+ (parent as? DigitalClockFaceView)?.invalidate()
+ }
+
+ override fun refreshTime() {
+ logger.d("refreshTime()")
+ refreshText()
+ }
+
+ override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ if (!this::textAnimator.isInitialized) {
+ return
+ }
+ val fvar = if (isDozing) aodStyle.fontVariation else textStyle.fontVariation
+ textAnimator.setTextStyle(
+ animate = isAnimated && isAnimationEnabled,
+ color = if (isDozing) aodFillColor else textFillColor,
+ textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+ fvar = fvar,
+ duration = aodStyle.transitionDuration,
+ interpolator = aodDozingInterpolator,
+ )
+ updateTextBoundsForTextAnimator()
+ outlineAnimator?.setTextStyle(
+ animate = isAnimated && isAnimationEnabled,
+ color = if (isDozing) aodOutlineColor else textOutlineColor,
+ textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+ fvar = fvar,
+ strokeWidth = if (isDozing) aodBorderWidth else textBorderWidth,
+ duration = aodStyle.transitionDuration,
+ interpolator = aodDozingInterpolator,
+ )
+ innerAnimator?.setTextStyle(
+ animate = isAnimated && isAnimationEnabled,
+ color = Color.WHITE,
+ textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+ fvar = fvar,
+ duration = aodStyle.transitionDuration,
+ interpolator = aodDozingInterpolator,
+ )
+ }
+
+ override fun animateCharge() {
+ if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
+ // Skip charge animation if dozing animation is already playing.
+ return
+ }
+ logger.d("animateCharge()")
+ val middleFvar = if (dozeFraction == 0F) aodStyle.fontVariation else textStyle.fontVariation
+ val endFvar = if (dozeFraction == 0F) textStyle.fontVariation else aodStyle.fontVariation
+ val startAnimPhase2 = Runnable {
+ textAnimator.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+ outlineAnimator?.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+ innerAnimator?.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+ updateTextBoundsForTextAnimator()
+ }
+ textAnimator.setTextStyle(
+ fvar = middleFvar,
+ animate = isAnimationEnabled,
+ onAnimationEnd = startAnimPhase2,
+ )
+ outlineAnimator?.setTextStyle(fvar = middleFvar, animate = isAnimationEnabled)
+ innerAnimator?.setTextStyle(fvar = middleFvar, animate = isAnimationEnabled)
+ updateTextBoundsForTextAnimator()
+ }
+
+ fun refreshText() {
+ lockScreenPaint.getTextBounds(text, 0, text.length, textBounds)
+ if (this::textAnimator.isInitialized) {
+ textAnimator.textInterpolator.targetPaint.getTextBounds(
+ text,
+ 0,
+ text.length,
+ targetTextBounds,
+ )
+ }
+ if (layout == null) {
+ requestLayout()
+ } else {
+ textAnimator.updateLayout(layout)
+ outlineAnimator?.updateLayout(layout)
+ innerAnimator?.updateLayout(layout)
+ }
+ }
+
+ private fun isSingleDigit(): Boolean {
+ return id == R.id.HOUR_FIRST_DIGIT ||
+ id == R.id.HOUR_SECOND_DIGIT ||
+ id == R.id.MINUTE_FIRST_DIGIT ||
+ id == R.id.MINUTE_SECOND_DIGIT
+ }
+
+ private fun updateInterpolatedTextBounds(): Rect {
+ val interpolatedTextBounds = Rect()
+ if (textAnimator.animator.animatedFraction != 1.0f && textAnimator.animator.isRunning) {
+ interpolatedTextBounds.left =
+ MathUtils.lerp(
+ prevTextBounds.left,
+ targetTextBounds.left,
+ textAnimator.animator.animatedValue as Float,
+ )
+ .toInt()
+
+ interpolatedTextBounds.right =
+ MathUtils.lerp(
+ prevTextBounds.right,
+ targetTextBounds.right,
+ textAnimator.animator.animatedValue as Float,
+ )
+ .toInt()
+
+ interpolatedTextBounds.top =
+ MathUtils.lerp(
+ prevTextBounds.top,
+ targetTextBounds.top,
+ textAnimator.animator.animatedValue as Float,
+ )
+ .toInt()
+
+ interpolatedTextBounds.bottom =
+ MathUtils.lerp(
+ prevTextBounds.bottom,
+ targetTextBounds.bottom,
+ textAnimator.animator.animatedValue as Float,
+ )
+ .toInt()
+ } else {
+ interpolatedTextBounds.set(targetTextBounds)
+ }
+ return interpolatedTextBounds
+ }
+
+ private fun updateXtranslation(inPoint: Point, interpolatedTextBounds: Rect): Point {
+ val viewWidth = if (isVertical) measuredHeight else measuredWidth
+ when (horizontalAlignment) {
+ HorizontalAlignment.LEFT -> {
+ inPoint.x = lockScreenPaint.strokeWidth.toInt() - interpolatedTextBounds.left
+ }
+ HorizontalAlignment.RIGHT -> {
+ inPoint.x =
+ viewWidth - interpolatedTextBounds.right - lockScreenPaint.strokeWidth.toInt()
+ }
+ HorizontalAlignment.CENTER -> {
+ inPoint.x =
+ (viewWidth - interpolatedTextBounds.width()) / 2 - interpolatedTextBounds.left
+ }
+ }
+ return inPoint
+ }
+
+ // translation of reference point of text
+ // used for translation when calling textInterpolator
+ fun getLocalTranslation(): Point {
+ val viewHeight = if (isVertical) measuredWidth else measuredHeight
+ val interpolatedTextBounds = updateInterpolatedTextBounds()
+ val localTranslation = Point(0, 0)
+ val correctedBaseline = if (baseline != -1) baseline else baselineFromMeasure
+ // get the change from current baseline to expected baseline
+ when (verticalAlignment) {
+ VerticalAlignment.CENTER -> {
+ localTranslation.y =
+ ((viewHeight - interpolatedTextBounds.height()) / 2 -
+ interpolatedTextBounds.top -
+ correctedBaseline)
+ }
+ VerticalAlignment.TOP -> {
+ localTranslation.y =
+ (-interpolatedTextBounds.top + lockScreenPaint.strokeWidth - correctedBaseline)
+ .toInt()
+ }
+ VerticalAlignment.BOTTOM -> {
+ localTranslation.y =
+ viewHeight -
+ interpolatedTextBounds.bottom -
+ lockScreenPaint.strokeWidth.toInt() -
+ correctedBaseline
+ }
+ VerticalAlignment.BASELINE -> {
+ localTranslation.y = -lockScreenPaint.strokeWidth.toInt()
+ }
+ }
+
+ return updateXtranslation(localTranslation, interpolatedTextBounds)
+ }
+
+ override fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) {
+ this.textStyle = textStyle as FontTextStyle
+ val typefaceName = "fonts/" + textStyle.fontFamily
+ setTypefaceCache(assets.typefaceCache.getVariantCache(typefaceName))
+ lockScreenPaint.strokeJoin = Paint.Join.ROUND
+ lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(textStyle.fontVariation)
+ textStyle.fontFeatureSettings?.let {
+ lockScreenPaint.fontFeatureSettings = it
+ fontFeatureSettings = it
+ }
+ typeface = lockScreenPaint.typeface
+ textStyle.lineHeight?.let { lineHeight = it.toInt() }
+ // borderWidth in textStyle and aodStyle is used to draw,
+ // strokeWidth in lockScreenPaint is used to measure and get enough space for the text
+ textStyle.borderWidth?.let { textBorderWidth = parser.convert(it) }
+
+ if (aodStyle != null && aodStyle is FontTextStyle) {
+ this.aodStyle = aodStyle
+ } else {
+ this.aodStyle = textStyle.copy()
+ }
+ this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it.interpolator }
+ aodBorderWidth = parser.convert(this.aodStyle.borderWidth ?: DEFAULT_AOD_STROKE_WIDTH)
+ lockScreenPaint.strokeWidth = ceil(max(textBorderWidth, aodBorderWidth))
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+ setInterpolatorPaint()
+ recomputeMaxSingleDigitSizes()
+ invalidate()
+ }
+
+ // When constrainedByHeight is on, targetFontSizePx is the constrained height of textView
+ override fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean) {
+ val adjustedFontSizePx = adjustFontSize(targetFontSizePx, constrainedByHeight)
+ val fontSizePx = adjustedFontSizePx * (textStyle.fontSizeScale ?: 1f)
+ aodFontSizePx =
+ adjustedFontSizePx * (aodStyle.fontSizeScale ?: textStyle.fontSizeScale ?: 1f)
+ if (fontSizePx > 0) {
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
+ lockScreenPaint.textSize = textSize
+ lockScreenPaint.getTextBounds(text, 0, text.length, textBounds)
+ targetTextBounds.set(textBounds)
+ }
+ if (!constrainedByHeight) {
+ val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
+ fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
+ }
+ textStyle.borderWidthScale?.let {
+ textBorderWidth = fontSizePx * it
+ if (dozeFraction < 1.0F) {
+ outlineAnimator?.setTextStyle(strokeWidth = textBorderWidth, animate = false)
+ }
+ }
+ aodStyle.borderWidthScale?.let {
+ aodBorderWidth = fontSizePx * it
+ if (dozeFraction > 0.0F) {
+ outlineAnimator?.setTextStyle(strokeWidth = aodBorderWidth, animate = false)
+ }
+ }
+
+ lockScreenPaint.strokeWidth = ceil(max(textBorderWidth, aodBorderWidth))
+ recomputeMaxSingleDigitSizes()
+
+ if (this::textAnimator.isInitialized) {
+ textAnimator.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+ }
+ outlineAnimator?.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+ innerAnimator?.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+ }
+
+ private fun recomputeMaxSingleDigitSizes() {
+ val rectForCalculate = Rect()
+ maxSingleDigitHeight = 0
+ maxSingleDigitWidth = 0
+
+ for (i in 0..9) {
+ lockScreenPaint.getTextBounds(i.toString(), 0, 1, rectForCalculate)
+ maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height())
+ maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width())
+ }
+ maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth.toInt()
+ maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth.toInt()
+ }
+
+ // called without animation, can be used to set the initial state of animator
+ private fun setInterpolatorPaint() {
+ if (this::textAnimator.isInitialized) {
+ // set initial style
+ textAnimator.textInterpolator.targetPaint.set(lockScreenPaint)
+ textAnimator.textInterpolator.onTargetPaintModified()
+ textAnimator.setTextStyle(
+ fvar = textStyle.fontVariation,
+ textSize = lockScreenPaint.textSize,
+ color = textFillColor,
+ animate = false,
+ )
+ }
+
+ if (outlineAnimator != null) {
+ outlineAnimator!!
+ .textInterpolator
+ .targetPaint
+ .set(
+ TextPaint(lockScreenPaint).also {
+ it.style =
+ if (aodStyle.renderType == RenderType.HOLLOW_TEXT)
+ Paint.Style.FILL_AND_STROKE
+ else Paint.Style.STROKE
+ }
+ )
+ outlineAnimator!!.textInterpolator.onTargetPaintModified()
+ outlineAnimator!!.setTextStyle(
+ fvar = aodStyle.fontVariation,
+ textSize = lockScreenPaint.textSize,
+ color = Color.TRANSPARENT,
+ animate = false,
+ )
+ }
+
+ if (innerAnimator != null) {
+ innerAnimator!!
+ .textInterpolator
+ .targetPaint
+ .set(TextPaint(lockScreenPaint).also { it.style = Paint.Style.FILL })
+ innerAnimator!!.textInterpolator.onTargetPaintModified()
+ innerAnimator!!.setTextStyle(
+ fvar = aodStyle.fontVariation,
+ textSize = lockScreenPaint.textSize,
+ color = Color.WHITE,
+ animate = false,
+ )
+ }
+ }
+
+ /* Called after textAnimator.setTextStyle
+ * textAnimator.setTextStyle will update targetPaint,
+ * and rebase if previous animator is canceled
+ * so basePaint will store the state we transition from
+ * and targetPaint will store the state we transition to
+ */
+ private fun updateTextBoundsForTextAnimator() {
+ textAnimator.textInterpolator.basePaint.getTextBounds(text, 0, text.length, prevTextBounds)
+ textAnimator.textInterpolator.targetPaint.getTextBounds(
+ text,
+ 0,
+ text.length,
+ targetTextBounds,
+ )
+ }
+
+ /*
+ * Adjust text size to adapt to large display / font size
+ * where the text view will be constrained by height
+ */
+ private fun adjustFontSize(targetFontSizePx: Float?, constrainedByHeight: Boolean): Float {
+ return if (constrainedByHeight) {
+ min((targetFontSizePx ?: 0F) / fontSizeAdjustFactor, lastUnconstrainedTextSize)
+ } else {
+ lastUnconstrainedTextSize = targetFontSizePx ?: 1F
+ lastUnconstrainedTextSize
+ }
+ }
+
+ companion object {
+ val DEFAULT_AOD_STROKE_WIDTH = "2dp"
+ val TEXT_OUTLINE_DEFAULT_COLOR = Color.TRANSPARENT
+ val AOD_DEFAULT_COLOR = Color.TRANSPARENT
+ val AOD_OUTLINE_DEFAULT_COLOR = Color.WHITE
+ private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0"
+ private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0"
+
+ fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) =
+ assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR)
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
new file mode 100644
index 000000000000..bbd2d3d1f782
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.shared.clocks.view
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.TextStyle
+
+interface SimpleDigitalClockView {
+ var text: String
+ var verticalAlignment: VerticalAlignment
+ var horizontalAlignment: HorizontalAlignment
+ var dozeFraction: Float
+ val textStyle: TextStyle
+ @VisibleForTesting var isAnimationEnabled: Boolean
+
+ fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?)
+
+ fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false)
+
+ fun updateColors(assets: AssetLoader, isRegionDark: Boolean)
+
+ fun refreshTime()
+
+ fun animateCharge()
+
+ fun animateDoze(isDozing: Boolean, isAnimated: Boolean)
+}
+
+enum class VerticalAlignment {
+ TOP,
+ BOTTOM,
+ BASELINE, // default
+ CENTER,
+}
+
+enum class HorizontalAlignment {
+ LEFT,
+ RIGHT,
+ CENTER, // default
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index d4d966ad2ef7..2312bbd2d7f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.nano.CommunalHubState
+import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.lifecycle.InstantTaskExecutorRule
import com.google.common.truth.Truth.assertThat
@@ -102,7 +103,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
widgetId = widgetId,
provider = provider,
rank = rank,
- userSerialNumber = userSerialNumber
+ userSerialNumber = userSerialNumber,
)
}
assertThat(widgets())
@@ -110,7 +111,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
communalItemRankEntry1,
communalWidgetItemEntry1,
communalItemRankEntry2,
- communalWidgetItemEntry2
+ communalWidgetItemEntry2,
)
}
@@ -129,7 +130,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- userSerialNumber = userSerialNumber
+ userSerialNumber = userSerialNumber,
)
}
@@ -165,7 +166,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
communalItemRankEntry1,
communalWidgetItemEntry1,
communalItemRankEntry2,
- communalWidgetItemEntry2
+ communalWidgetItemEntry2,
)
communalWidgetDao.deleteWidgetById(communalWidgetItemEntry1.widgetId)
@@ -251,6 +252,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
componentName = "pk_name/cls_name_4",
itemId = 4L,
userSerialNumber = 0,
+ spanY = 3,
)
assertThat(widgets())
.containsExactly(
@@ -267,6 +269,68 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
}
@Test
+ fun addWidget_withDifferentSpanY_readsCorrectValuesInDb() =
+ testScope.runTest {
+ val widgets = collectLastValue(communalWidgetDao.getWidgets())
+
+ // Add widgets with different spanY values
+ communalWidgetDao.addWidget(
+ widgetId = 1,
+ provider = ComponentName("pkg_name", "cls_name_1"),
+ rank = 0,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.FULL.span,
+ )
+ communalWidgetDao.addWidget(
+ widgetId = 2,
+ provider = ComponentName("pkg_name", "cls_name_2"),
+ rank = 1,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.HALF.span,
+ )
+ communalWidgetDao.addWidget(
+ widgetId = 3,
+ provider = ComponentName("pkg_name", "cls_name_3"),
+ rank = 2,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.THIRD.span,
+ )
+
+ // Verify that the widgets have the correct spanY values
+ assertThat(widgets())
+ .containsExactly(
+ CommunalItemRank(uid = 1L, rank = 0),
+ CommunalWidgetItem(
+ uid = 1L,
+ widgetId = 1,
+ componentName = "pkg_name/cls_name_1",
+ itemId = 1L,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.FULL.span,
+ ),
+ CommunalItemRank(uid = 2L, rank = 1),
+ CommunalWidgetItem(
+ uid = 2L,
+ widgetId = 2,
+ componentName = "pkg_name/cls_name_2",
+ itemId = 2L,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.HALF.span,
+ ),
+ CommunalItemRank(uid = 3L, rank = 2),
+ CommunalWidgetItem(
+ uid = 3L,
+ widgetId = 3,
+ componentName = "pkg_name/cls_name_3",
+ itemId = 3L,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.THIRD.span,
+ ),
+ )
+ .inOrder()
+ }
+
+ @Test
fun restoreCommunalHubState() =
testScope.runTest {
// Set up db
@@ -288,6 +352,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
componentName = fakeWidget.componentName,
itemId = rank.uid,
userSerialNumber = fakeWidget.userSerialNumber,
+ spanY = 3,
)
expected[rank] = widget
}
@@ -343,6 +408,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
componentName = widgetInfo1.provider.flattenToString(),
itemId = communalItemRankEntry1.uid,
userSerialNumber = widgetInfo1.userSerialNumber,
+ spanY = 3,
)
val communalWidgetItemEntry2 =
CommunalWidgetItem(
@@ -351,6 +417,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
componentName = widgetInfo2.provider.flattenToString(),
itemId = communalItemRankEntry2.uid,
userSerialNumber = widgetInfo2.userSerialNumber,
+ spanY = 3,
)
val communalWidgetItemEntry3 =
CommunalWidgetItem(
@@ -359,6 +426,7 @@ class CommunalWidgetDaoTest : SysuiTestCase() {
componentName = widgetInfo3.provider.flattenToString(),
itemId = communalItemRankEntry3.uid,
userSerialNumber = widgetInfo3.userSerialNumber,
+ spanY = 3,
)
val fakeState =
CommunalHubState().apply {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
index eba395bdb5a3..596db0767867 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
@@ -117,6 +117,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
componentName = defaultWidgets[0],
rank = 0,
userSerialNumber = 0,
+ spanY = 3,
)
verify(communalWidgetDao)
.addWidget(
@@ -124,6 +125,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
componentName = defaultWidgets[1],
rank = 1,
userSerialNumber = 0,
+ spanY = 3,
)
verify(communalWidgetDao)
.addWidget(
@@ -131,6 +133,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
componentName = defaultWidgets[2],
rank = 2,
userSerialNumber = 0,
+ spanY = 3,
)
}
@@ -152,6 +155,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
componentName = any(),
rank = anyInt(),
userSerialNumber = anyInt(),
+ spanY = anyInt(),
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index 980a5ec8c494..3d30eccc4572 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -143,7 +143,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
fun communalWidgets_queryWidgetsFromDb() =
testScope.runTest {
val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
- val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L, 0)
+ val communalWidgetItemEntry =
+ CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L, 0, 3)
fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry)
fakeProviders.value = mapOf(1 to providerInfoA)
@@ -169,19 +170,15 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
fakeWidgets.value =
mapOf(
CommunalItemRank(uid = 1L, rank = 1) to
- CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3),
CommunalItemRank(uid = 2L, rank = 2) to
- CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3),
CommunalItemRank(uid = 3L, rank = 3) to
- CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L, 0),
+ CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L, 0, 3),
CommunalItemRank(uid = 4L, rank = 4) to
- CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0),
- )
- fakeProviders.value =
- mapOf(
- 1 to providerInfoA,
- 2 to providerInfoB,
+ CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0, 3),
)
+ fakeProviders.value = mapOf(1 to providerInfoA, 2 to providerInfoB)
// Expect to see only widget 1 and 2
val communalWidgets by collectLastValue(underTest.communalWidgets)
@@ -207,15 +204,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
fakeWidgets.value =
mapOf(
CommunalItemRank(uid = 1L, rank = 1) to
- CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3),
CommunalItemRank(uid = 2L, rank = 2) to
- CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
- )
- fakeProviders.value =
- mapOf(
- 1 to providerInfoA,
- 2 to providerInfoB,
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3),
)
+ fakeProviders.value = mapOf(1 to providerInfoA, 2 to providerInfoB)
// Expect two widgets
val communalWidgets by collectLastValue(underTest.communalWidgets)
@@ -235,11 +228,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
)
// Provider info updated for widget 1
- fakeProviders.value =
- mapOf(
- 1 to providerInfoC,
- 2 to providerInfoB,
- )
+ fakeProviders.value = mapOf(1 to providerInfoC, 2 to providerInfoB)
runCurrent()
assertThat(communalWidgets)
@@ -269,7 +258,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -294,7 +283,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -303,7 +292,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
verify(communalWidgetDao, never())
- .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt())
+ .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt(), anyInt())
verify(appWidgetHost).deleteAppWidgetId(id)
// Verify backup not requested
@@ -321,7 +310,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -332,7 +321,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
verify(communalWidgetDao, never())
- .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt())
+ .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt(), anyInt())
verify(appWidgetHost).deleteAppWidgetId(id)
// Verify backup not requested
@@ -350,7 +339,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -650,8 +639,10 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
eq(newWidgetId),
componentNameCaptor.capture(),
eq(2),
- eq(testUserSerialNumber(workProfile))
+ eq(testUserSerialNumber(workProfile)),
+ anyInt(),
)
+
assertThat(componentNameCaptor.firstValue)
.isEqualTo(ComponentName("pk_name", "fake_widget_2"))
}
@@ -662,9 +653,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
fakeWidgets.value =
mapOf(
CommunalItemRank(uid = 1L, rank = 1) to
- CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3),
CommunalItemRank(uid = 2L, rank = 2) to
- CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3),
)
// Widget 1 is installed
@@ -707,7 +698,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
fakeWidgets.value =
mapOf(
CommunalItemRank(uid = 1L, rank = 1) to
- CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3)
)
// Widget 1 is pending install
@@ -732,7 +723,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
componentName = ComponentName("pk_1", "cls_1"),
icon = fakeIcon,
user = mainUser,
- ),
+ )
)
// Package for widget 1 finished installing
@@ -749,10 +740,23 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
appWidgetId = 1,
providerInfo = providerInfoA,
rank = 1,
- ),
+ )
)
}
+ @Test
+ fun updateWidgetSpanY_updatesWidgetInDaoAndRequestsBackup() =
+ testScope.runTest {
+ val widgetId = 1
+ val newSpanY = 6
+
+ underTest.updateWidgetSpanY(widgetId, newSpanY)
+ runCurrent()
+
+ verify(communalWidgetDao).updateWidgetSpanY(widgetId, newSpanY)
+ verify(backupManager).dataChanged()
+ }
+
private fun setAppWidgetIds(ids: List<Int>) {
whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
index 8f9e23824809..8b1341114c68 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
@@ -145,7 +145,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
fakeInputManager.addPhysicalKeyboard(
PHYSICAL_NOT_FULL_KEYBOARD_ID,
- isFullKeyboard = false
+ isFullKeyboard = false,
)
assertThat(isKeyboardConnected).isFalse()
@@ -223,7 +223,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
backlightListenerCaptor.value.onBacklightChanged(
current = 1,
max = 5,
- triggeredByKeyPress = false
+ triggeredByKeyPress = false,
)
assertThat(backlight).isNull()
}
@@ -239,7 +239,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
backlightListenerCaptor.value.onBacklightChanged(
current = 1,
max = 5,
- triggeredByKeyPress = true
+ triggeredByKeyPress = true,
)
assertThat(backlight).isNotNull()
}
@@ -318,15 +318,75 @@ class KeyboardRepositoryTest : SysuiTestCase() {
}
}
+ @Test
+ fun connectedKeyboards_emitsAllKeyboards() {
+ testScope.runTest {
+ val firstKeyboard = Keyboard(vendorId = 1, productId = 1)
+ val secondKeyboard = Keyboard(vendorId = 2, productId = 2)
+ captureDeviceListener()
+ val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+ fakeInputManager.addPhysicalKeyboard(
+ PHYSICAL_FULL_KEYBOARD_ID,
+ vendorId = firstKeyboard.vendorId,
+ productId = firstKeyboard.productId,
+ )
+ assertThat(keyboards)
+ .containsExactly(Keyboard(firstKeyboard.vendorId, firstKeyboard.productId))
+
+ fakeInputManager.addPhysicalKeyboard(
+ ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
+ vendorId = secondKeyboard.vendorId,
+ productId = secondKeyboard.productId,
+ )
+ assertThat(keyboards)
+ .containsExactly(
+ Keyboard(firstKeyboard.vendorId, firstKeyboard.productId),
+ Keyboard(secondKeyboard.vendorId, secondKeyboard.productId),
+ )
+ }
+ }
+
+ @Test
+ fun connectedKeyboards_emitsOnlyFullPhysicalKeyboards() {
+ testScope.runTest {
+ captureDeviceListener()
+ val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
+ fakeInputManager.addPhysicalKeyboard(
+ PHYSICAL_NOT_FULL_KEYBOARD_ID,
+ isFullKeyboard = false,
+ )
+
+ assertThat(keyboards).hasSize(1)
+ }
+ }
+
+ @Test
+ fun connectedKeyboards_emitsOnlyConnectedKeyboards() {
+ testScope.runTest {
+ captureDeviceListener()
+ val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.removeDevice(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+
+ assertThat(keyboards).hasSize(1)
+ }
+ }
+
private fun KeyboardBacklightListener.onBacklightChanged(
current: Int,
max: Int,
- triggeredByKeyPress: Boolean = true
+ triggeredByKeyPress: Boolean = true,
) {
onKeyboardBacklightChanged(
/* deviceId= */ 0,
TestBacklightState(current, max),
- triggeredByKeyPress
+ triggeredByKeyPress,
)
}
@@ -343,7 +403,7 @@ class KeyboardRepositoryTest : SysuiTestCase() {
private class TestBacklightState(
private val brightnessLevel: Int,
- private val maxBrightnessLevel: Int
+ private val maxBrightnessLevel: Int,
) : KeyboardBacklightState() {
override fun getBrightnessLevel() = brightnessLevel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 523a89ad5740..5b0b59de47c2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -71,7 +71,11 @@ import androidx.test.filters.SmallTest;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
+import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.DisableSceneContainer;
+import com.android.systemui.flags.EnableSceneContainer;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
import com.android.systemui.log.LogWtfHandlerRule;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -90,6 +94,10 @@ import com.android.systemui.util.time.FakeSystemClock;
import com.google.android.collect.Lists;
+import dagger.Lazy;
+
+import kotlinx.coroutines.flow.StateFlow;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -152,6 +160,12 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
private BroadcastDispatcher mBroadcastDispatcher;
@Mock
private KeyguardStateController mKeyguardStateController;
+ @Mock
+ private Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+ @Mock
+ private DeviceUnlockedInteractor mDeviceUnlockedInteractor;
+ @Mock
+ private StateFlow<DeviceUnlockStatus> mDeviceUnlockStatusStateFlow;
private UserInfo mCurrentUser;
private UserInfo mSecondaryUser;
@@ -238,6 +252,9 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
mLockscreenUserManager.setUpWithPresenter(mPresenter);
+ when(mDeviceUnlockedInteractor.getDeviceUnlockStatus())
+ .thenReturn(mDeviceUnlockStatusStateFlow);
+
mBackgroundExecutor.runAllReady();
}
@@ -493,7 +510,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
}
@Test
- public void testUpdateIsPublicMode() {
+ @DisableSceneContainer
+ public void testUpdateIsPublicMode_sceneContainerDisabled() {
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
when(mKeyguardStateController.isShowing()).thenReturn(false);
@@ -527,6 +545,57 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
mBackgroundExecutor.runAllReady();
assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
verify(listener, never()).onNotificationStateChanged();
+
+ verify(mDeviceUnlockedInteractorLazy, never()).get();
+ }
+
+ @Test
+ @EnableSceneContainer
+ public void testUpdateIsPublicMode_sceneContainerEnabled() {
+ when(mDeviceUnlockedInteractorLazy.get()).thenReturn(mDeviceUnlockedInteractor);
+
+ // device is unlocked
+ when(mDeviceUnlockStatusStateFlow.getValue()).thenReturn(new DeviceUnlockStatus(
+ /* isUnlocked = */ true,
+ /* deviceUnlockSource = */ null
+ ));
+
+ NotificationStateChangedListener listener = mock(NotificationStateChangedListener.class);
+ mLockscreenUserManager.addNotificationStateChangedListener(listener);
+ mLockscreenUserManager.mCurrentProfiles.append(0, mock(UserInfo.class));
+
+ // first call explicitly sets user 0 to not public; notifies
+ mLockscreenUserManager.updatePublicMode();
+ mBackgroundExecutor.runAllReady();
+ assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
+ verify(listener).onNotificationStateChanged();
+ clearInvocations(listener);
+
+ // calling again has no changes; does not notify
+ mLockscreenUserManager.updatePublicMode();
+ mBackgroundExecutor.runAllReady();
+ assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
+ verify(listener, never()).onNotificationStateChanged();
+
+ // device is not unlocked
+ when(mDeviceUnlockStatusStateFlow.getValue()).thenReturn(new DeviceUnlockStatus(
+ /* isUnlocked = */ false,
+ /* deviceUnlockSource = */ null
+ ));
+
+ // Calling again with device now not unlocked makes user 0 public; notifies
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ mLockscreenUserManager.updatePublicMode();
+ mBackgroundExecutor.runAllReady();
+ assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
+ verify(listener).onNotificationStateChanged();
+ clearInvocations(listener);
+
+ // calling again has no changes; does not notify
+ mLockscreenUserManager.updatePublicMode();
+ mBackgroundExecutor.runAllReady();
+ assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
+ verify(listener, never()).onNotificationStateChanged();
}
@Test
@@ -972,7 +1041,9 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
mSettings,
mock(DumpManager.class),
mock(LockPatternUtils.class),
- mFakeFeatureFlags);
+ mFakeFeatureFlags,
+ mDeviceUnlockedInteractorLazy
+ );
}
public BroadcastReceiver getBaseBroadcastReceiverForTest() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
index 28857a08c2bd..34f46088ad79 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -208,8 +208,8 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(footerVisible).isTrue()
}
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
@Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun onClick_whenHistoryDisabled_leadsToSettingsPage() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -222,8 +222,8 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(onClick?.backStack).isEmpty()
}
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
@Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun onClick_whenHistoryEnabled_leadsToHistoryPage() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -237,8 +237,8 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
.containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS)
}
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
@Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -263,8 +263,8 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
.containsExactly(Settings.ACTION_ZEN_MODE_SETTINGS)
}
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
@Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json
new file mode 100644
index 000000000000..c3fb8d4d5ab6
--- /dev/null
+++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json
@@ -0,0 +1,88 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "a49f2f7d25cf12d1baf9a3a3e6243b64",
+ "entities": [
+ {
+ "tableName": "communal_widget_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL, `user_serial_number` INTEGER NOT NULL DEFAULT -1, `span_y` INTEGER NOT NULL DEFAULT 3)",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "widgetId",
+ "columnName": "widget_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "componentName",
+ "columnName": "component_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "itemId",
+ "columnName": "item_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userSerialNumber",
+ "columnName": "user_serial_number",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "spanY",
+ "columnName": "span_y",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "3"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uid"
+ ]
+ }
+ },
+ {
+ "tableName": "communal_item_rank_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rank",
+ "columnName": "rank",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uid"
+ ]
+ }
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a49f2f7d25cf12d1baf9a3a3e6243b64')"
+ ]
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index aabfbd11b70e..65c01ed9eecd 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -710,9 +710,16 @@ public class ClipboardOverlayController implements ClipboardListener.ClipboardOv
@Override
public void onShareButtonTapped() {
if (clipboardSharedTransitions()) {
- if (mClipboardModel.getType() != ClipboardModel.Type.OTHER) {
- finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
- IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+ switch (mClipboardModel.getType()) {
+ case TEXT:
+ case URI:
+ finish(CLIPBOARD_OVERLAY_SHARE_TAPPED,
+ IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+ break;
+ case IMAGE:
+ finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
+ IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+ break;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
index 8f1854f93fe4..17f4f0c83d6f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
@@ -26,7 +26,7 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.android.systemui.res.R
-@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 3)
+@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 4)
abstract class CommunalDatabase : RoomDatabase() {
abstract fun communalWidgetDao(): CommunalWidgetDao
@@ -43,19 +43,16 @@ abstract class CommunalDatabase : RoomDatabase() {
* @param callback An optional callback registered to the database. Only effective when a
* new instance is created.
*/
- fun getInstance(
- context: Context,
- callback: Callback? = null,
- ): CommunalDatabase {
+ fun getInstance(context: Context, callback: Callback? = null): CommunalDatabase {
if (instance == null) {
instance =
Room.databaseBuilder(
context,
CommunalDatabase::class.java,
- context.resources.getString(R.string.config_communalDatabase)
+ context.resources.getString(R.string.config_communalDatabase),
)
.also { builder ->
- builder.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
+ builder.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
builder.fallbackToDestructiveMigration(dropAllTables = true)
callback?.let { callback -> builder.addCallback(callback) }
}
@@ -103,5 +100,21 @@ abstract class CommunalDatabase : RoomDatabase() {
)
}
}
+
+ /**
+ * This migration adds a span_y column to the communal_widget_table and sets its default
+ * value to 3.
+ */
+ @VisibleForTesting
+ val MIGRATION_3_4 =
+ object : Migration(3, 4) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ Log.i(TAG, "Migrating from version 3 to 4")
+ db.execSQL(
+ "ALTER TABLE communal_widget_table " +
+ "ADD COLUMN span_y INTEGER NOT NULL DEFAULT 3"
+ )
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
index e33aead11842..f9d2a843c213 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
@@ -40,6 +40,12 @@ data class CommunalWidgetItem(
*/
@ColumnInfo(name = "user_serial_number", defaultValue = "$USER_SERIAL_NUMBER_UNDEFINED")
val userSerialNumber: Int,
+
+ /**
+ * The vertical span of the widget. Span_Y default value corresponds to
+ * CommunalContentSize.HALF.span
+ */
+ @ColumnInfo(name = "span_y", defaultValue = "3") val spanY: Int,
) {
companion object {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index 93b86bd10133..5dd4c1cb7f72 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -25,6 +25,7 @@ import androidx.room.RoomDatabase
import androidx.room.Transaction
import androidx.sqlite.db.SupportSQLiteDatabase
import com.android.systemui.communal.nano.CommunalHubState
+import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.widgets.CommunalWidgetHost
import com.android.systemui.communal.widgets.CommunalWidgetModule.Companion.DEFAULT_WIDGETS
import com.android.systemui.dagger.SysUISingleton
@@ -153,14 +154,15 @@ interface CommunalWidgetDao {
@Query(
"INSERT INTO communal_widget_table" +
- "(widget_id, component_name, item_id, user_serial_number) " +
- "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber)"
+ "(widget_id, component_name, item_id, user_serial_number, span_y) " +
+ "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber, :spanY)"
)
fun insertWidget(
widgetId: Int,
componentName: String,
itemId: Long,
userSerialNumber: Int,
+ spanY: Int = 3,
): Long
@Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)")
@@ -169,6 +171,9 @@ interface CommunalWidgetDao {
@Query("UPDATE communal_item_rank_table SET rank = :order WHERE uid = :itemUid")
fun updateItemRank(itemUid: Long, order: Int)
+ @Query("UPDATE communal_widget_table SET span_y = :spanY WHERE widget_id = :widgetId")
+ fun updateWidgetSpanY(widgetId: Int, spanY: Int)
+
@Query("DELETE FROM communal_widget_table") fun clearCommunalWidgetsTable()
@Query("DELETE FROM communal_item_rank_table") fun clearCommunalItemRankTable()
@@ -189,12 +194,14 @@ interface CommunalWidgetDao {
provider: ComponentName,
rank: Int? = null,
userSerialNumber: Int,
+ spanY: Int = CommunalContentSize.HALF.span,
): Long {
return addWidget(
widgetId = widgetId,
componentName = provider.flattenToString(),
rank = rank,
userSerialNumber = userSerialNumber,
+ spanY = spanY,
)
}
@@ -204,6 +211,7 @@ interface CommunalWidgetDao {
componentName: String,
rank: Int? = null,
userSerialNumber: Int,
+ spanY: Int = 3,
): Long {
val widgets = getWidgetsNow()
@@ -224,6 +232,7 @@ interface CommunalWidgetDao {
componentName = componentName,
itemId = insertItemRank(newRank),
userSerialNumber = userSerialNumber,
+ spanY = spanY,
)
}
@@ -246,7 +255,8 @@ interface CommunalWidgetDao {
clearCommunalItemRankTable()
state.widgets.forEach {
- addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber)
+ val spanY = if (it.spanY != 0) it.spanY else CommunalContentSize.HALF.span
+ addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber, spanY)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 6cdd9fffe077..3312f3cac64b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -92,6 +92,14 @@ interface CommunalWidgetRepository {
/** Aborts the restore process and removes files from disk if necessary. */
fun abortRestoreWidgets()
+
+ /**
+ * Update the spanY of a widget in the database.
+ *
+ * @param widgetId id of the widget to update.
+ * @param spanY new spanY value for the widget.
+ */
+ fun updateWidgetSpanY(widgetId: Int, spanY: Int)
}
@SysUISingleton
@@ -118,20 +126,30 @@ constructor(
/** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */
private val widgetEntries: Flow<List<CommunalWidgetEntry>> =
- combine(
- communalWidgetDao.getWidgets(),
- communalWidgetHost.appWidgetProviders,
- ) { entries, providers ->
+ combine(communalWidgetDao.getWidgets(), communalWidgetHost.appWidgetProviders) {
+ entries,
+ providers ->
entries.mapNotNull { (rank, widget) ->
CommunalWidgetEntry(
appWidgetId = widget.widgetId,
componentName = widget.componentName,
rank = rank.rank,
- providerInfo = providers[widget.widgetId]
+ providerInfo = providers[widget.widgetId],
)
}
}
+ override fun updateWidgetSpanY(widgetId: Int, spanY: Int) {
+ bgScope.launch {
+ communalWidgetDao.updateWidgetSpanY(widgetId, spanY)
+ logger.i({ "Updated spanY of widget $int1 to $int2." }) {
+ int1 = widgetId
+ int2 = spanY
+ }
+ backupManager.dataChanged()
+ }
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
widgetEntries
@@ -197,6 +215,7 @@ constructor(
provider = provider,
rank = rank,
userSerialNumber = userManager.getUserSerialNumber(user.identifier),
+ spanY = 3,
)
backupManager.dataChanged()
} else {
@@ -325,6 +344,7 @@ constructor(
componentName = restoredWidget.componentName
rank = restoredWidget.rank
userSerialNumber = userManager.getUserSerialNumber(newUser.identifier)
+ spanY = restoredWidget.spanY
}
}
val newState = CommunalHubState().apply { widgets = newWidgets.toTypedArray() }
@@ -383,6 +403,7 @@ constructor(
appWidgetId = entry.appWidgetId,
providerInfo = entry.providerInfo!!,
rank = entry.rank,
+ spanY = entry.spanY,
)
}
@@ -400,6 +421,7 @@ constructor(
appWidgetId = entry.appWidgetId,
providerInfo = entry.providerInfo!!,
rank = entry.rank,
+ spanY = entry.spanY,
)
}
@@ -412,6 +434,7 @@ constructor(
componentName = componentName,
icon = session.icon,
user = session.user,
+ spanY = entry.spanY,
)
} else {
null
@@ -423,5 +446,6 @@ constructor(
val componentName: String,
val rank: Int,
var providerInfo: AppWidgetProviderInfo? = null,
+ var spanY: Int = 3,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
index bc14ae1eaff4..7602a7afce4e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
+++ b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
@@ -38,5 +38,8 @@ message CommunalHubState {
// Serial number of the user associated with the widget.
int32 user_serial_number = 4;
+
+ // The vertical span of the widget
+ int32 span_y = 5;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
index 63b1a14b3135..bcbc8f65ce36 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
@@ -31,6 +31,7 @@ sealed interface CommunalWidgetContentModel {
override val appWidgetId: Int,
val providerInfo: AppWidgetProviderInfo,
override val rank: Int,
+ val spanY: Int = 3,
) : CommunalWidgetContentModel
/** Widget is pending installation */
@@ -40,5 +41,6 @@ sealed interface CommunalWidgetContentModel {
val componentName: ComponentName,
val icon: Bitmap?,
val user: UserHandle,
+ val spanY: Int = 3,
) : CommunalWidgetContentModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index a94fbd937afc..a5b22775f3d5 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -56,6 +56,7 @@ import com.android.systemui.rotationlock.RotationLockNewModule;
import com.android.systemui.scene.SceneContainerFrameworkModule;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
import com.android.systemui.settings.MultiUserUtilsModule;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
import com.android.systemui.shade.ShadeModule;
import com.android.systemui.startable.Dependencies;
@@ -178,9 +179,9 @@ public abstract class ReferenceSystemUIModule {
@Provides
@SysUISingleton
static IndividualSensorPrivacyController provideIndividualSensorPrivacyController(
- SensorPrivacyManager sensorPrivacyManager) {
+ SensorPrivacyManager sensorPrivacyManager, UserTracker userTracker) {
IndividualSensorPrivacyController spC = new IndividualSensorPrivacyControllerImpl(
- sensorPrivacyManager);
+ sensorPrivacyManager, userTracker);
spC.init();
return spC;
}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
index 5a008bddc748..7711c48924cc 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
@@ -38,7 +38,7 @@ class InputDeviceRepository
constructor(
@Background private val backgroundHandler: Handler,
@Background private val backgroundScope: CoroutineScope,
- private val inputManager: InputManager
+ private val inputManager: InputManager,
) {
sealed interface DeviceChange
@@ -50,11 +50,11 @@ constructor(
data object FreshStart : DeviceChange
/**
- * Emits collection of all currently connected keyboards and what was the last [DeviceChange].
- * It emits collection so that every new subscriber to this SharedFlow can get latest state of
- * all keyboards. Otherwise we might get into situation where subscriber timing on
- * initialization matter and later subscriber will only get latest device and will miss all
- * previous devices.
+ * Emits collection of all currently connected input devices and what was the last
+ * [DeviceChange]. It emits collection so that every new subscriber to this SharedFlow can get
+ * latest state of all input devices. Otherwise we might get into situation where subscriber
+ * timing on initialization matter and later subscriber will only get latest device and will
+ * miss all previous devices.
*/
// TODO(b/351984587): Replace with StateFlow
@SuppressLint("SharedFlowCreation")
@@ -79,11 +79,7 @@ constructor(
inputManager.registerInputDeviceListener(listener, backgroundHandler)
awaitClose { inputManager.unregisterInputDeviceListener(listener) }
}
- .shareIn(
- scope = backgroundScope,
- started = SharingStarted.Lazily,
- replay = 1,
- )
+ .shareIn(scope = backgroundScope, started = SharingStarted.Lazily, replay = 1)
private fun <T> SendChannel<T>.sendWithLogging(element: T) {
trySendWithFailureLogging(element, TAG)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
index f49cfdda8b0a..021c069008c3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
@@ -50,6 +50,8 @@ class CommandLineKeyboardRepository @Inject constructor(commandRegistry: Command
private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
+ override val connectedKeyboards: Flow<Set<Keyboard>> = MutableStateFlow(emptySet())
+
init {
Log.i(TAG, "initializing shell command $COMMAND")
commandRegistry.registerCommand(COMMAND) { KeyboardCommand() }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index a20dfa5a4c3e..3329fe29dade 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -61,6 +61,9 @@ interface KeyboardRepository {
*/
val newlyConnectedKeyboard: Flow<Keyboard>
+ /** Emits set of currently connected keyboards */
+ val connectedKeyboards: Flow<Set<Keyboard>>
+
/**
* Emits [BacklightModel] whenever user changes backlight level from keyboard press. Can only
* happen when physical keyboard is connected
@@ -74,7 +77,7 @@ class KeyboardRepositoryImpl
constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val inputManager: InputManager,
- inputDeviceRepository: InputDeviceRepository
+ inputDeviceRepository: InputDeviceRepository,
) : KeyboardRepository {
@FlowPreview
@@ -93,6 +96,13 @@ constructor(
.mapNotNull { deviceIdToKeyboard(it) }
.flowOn(backgroundDispatcher)
+ override val connectedKeyboards: Flow<Set<Keyboard>> =
+ inputDeviceRepository.deviceChange
+ .map { (deviceIds, _) -> deviceIds }
+ .map { deviceIds -> deviceIds.filter { isPhysicalFullKeyboard(it) } }
+ .distinctUntilChanged()
+ .map { deviceIds -> deviceIds.mapNotNull { deviceIdToKeyboard(it) }.toSet() }
+
override val isAnyKeyboardConnected: Flow<Boolean> =
inputDeviceRepository.deviceChange
.map { (ids, _) -> ids.any { id -> isPhysicalFullKeyboard(id) } }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 38608d0e793a..ab8a9539b7f2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -202,10 +202,10 @@ constructor(
// Return the single display to be screenshot based upon the request.
private suspend fun getDisplayToScreenshot(screenshotRequest: ScreenshotRequest): Display {
return when (screenshotRequest.source) {
- // TODO(b/367394043): Overview requests should use a display ID provided in
- // ScreenshotRequest.
ScreenshotSource.SCREENSHOT_OVERVIEW ->
- displayRepository.getDisplay(Display.DEFAULT_DISPLAY)
+ // Show on the display where overview was shown if available.
+ displayRepository.getDisplay(screenshotRequest.displayId)
+ ?: displayRepository.getDisplay(Display.DEFAULT_DISPLAY)
?: error("Can't find default display")
// Key chord and vendor gesture occur on the device itself, so screenshot the device's
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 4f47536f6b32..f83548ddbf45 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -994,7 +994,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
// be dropped, causing the shade expansion to fail silently. Since the shade doesn't open,
// it doesn't become visible, and the bounds will never update. Therefore, we must detect
// the incorrect bounds here and force the update so that touches are routed correctly.
- if (SceneContainerFlag.isEnabled() && mWindowRootView.getVisibility() == View.INVISIBLE) {
+ if (SceneContainerFlag.isEnabled()
+ && mWindowRootView != null
+ && mWindowRootView.getVisibility() == View.INVISIBLE) {
Rect bounds = newConfig.windowConfiguration.getBounds();
if (mWindowRootView.getWidth() != bounds.width()) {
mLogger.logConfigChangeWidthAdjust(mWindowRootView.getWidth(), bounds.width());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 7244f8a64c19..e47952fd6c4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -62,11 +62,13 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlagsClassic;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -286,6 +288,8 @@ public class NotificationLockscreenUserManagerImpl implements
protected ContentObserver mLockscreenSettingsObserver;
protected ContentObserver mSettingsObserver;
+ private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+
@Inject
public NotificationLockscreenUserManagerImpl(Context context,
BroadcastDispatcher broadcastDispatcher,
@@ -305,7 +309,8 @@ public class NotificationLockscreenUserManagerImpl implements
SecureSettings secureSettings,
DumpManager dumpManager,
LockPatternUtils lockPatternUtils,
- FeatureFlagsClassic featureFlags) {
+ FeatureFlagsClassic featureFlags,
+ Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy) {
mContext = context;
mMainExecutor = mainExecutor;
mBackgroundExecutor = backgroundExecutor;
@@ -325,6 +330,7 @@ public class NotificationLockscreenUserManagerImpl implements
mSecureSettings = secureSettings;
mKeyguardStateController = keyguardStateController;
mFeatureFlags = featureFlags;
+ mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy;
mLockScreenUris.add(SHOW_LOCKSCREEN);
mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN);
@@ -748,8 +754,13 @@ public class NotificationLockscreenUserManagerImpl implements
// camera on the keyguard has a state of SHADE but the keyguard is still showing.
final boolean showingKeyguard = mState != StatusBarState.SHADE
|| mKeyguardStateController.isShowing();
- final boolean devicePublic = showingKeyguard && mKeyguardStateController.isMethodSecure();
-
+ final boolean devicePublic;
+ if (SceneContainerFlag.isEnabled()) {
+ devicePublic = !mDeviceUnlockedInteractorLazy.get()
+ .getDeviceUnlockStatus().getValue().isUnlocked();
+ } else {
+ devicePublic = showingKeyguard && mKeyguardStateController.isMethodSecure();
+ }
// Look for public mode users. Users are considered public in either case of:
// - device keyguard is shown in secure mode;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 5d14be8c974c..73ad0e50793a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -272,7 +272,7 @@ public class StatusBarStateControllerImpl implements
* Updates the {@link StatusBarState} and notifies registered listeners, if needed.
*/
private void updateStateAndNotifyListeners(int state) {
- if (state != mUpcomingState) {
+ if (state != mUpcomingState && !SceneContainerFlag.isEnabled()) {
Log.d(TAG, "setState: requested state " + StatusBarState.toString(state)
+ "!= upcomingState: " + StatusBarState.toString(mUpcomingState) + ". "
+ "This usually means the status bar state transition was interrupted before "
@@ -728,20 +728,23 @@ public class StatusBarStateControllerImpl implements
// doesn't work well for clients of this class (like remote input) that expect the device to
// be fully and properly unlocked when the state changes to SHADE.
//
- // Therefore, we calculate the device to be in a locked-ish state (KEYGUARD or SHADE_LOCKED,
+ // Therefore, we consider the device to be in a keyguardish state (KEYGUARD or SHADE_LOCKED,
// but not SHADE) if *any* of these are still true:
// 1. deviceUnlockStatus.isUnlocked is false.
- // 2. We are on (currentScene equals) a locked-ish scene (Lockscreen, Bouncer, or Communal).
- // 3. We are over (backStack contains) a locked-ish scene (Lockscreen or Communal).
+ // 2. currentScene is a keyguardish scene (Lockscreen, Bouncer, or Communal).
+ // 3. backStack contains a keyguardish scene (Lockscreen or Communal).
+
+ final boolean onKeyguardish = onLockscreen || onBouncer || onCommunal;
+ final boolean overKeyguardish = overLockscreen || overCommunal;
if (isOccluded) {
// Occlusion is special; even though the device is still technically on the lockscreen,
// the UI behaves as if it is unlocked.
newState = StatusBarState.SHADE;
- } else if (onLockscreen || onBouncer || onCommunal || overLockscreen || overCommunal) {
- // We get here if we are on or over a locked-ish scene, even if isUnlocked is true; we
+ } else if (onKeyguardish || overKeyguardish) {
+ // We get here if we are on or over a keyguardish scene, even if isUnlocked is true; we
// want to return SHADE_LOCKED or KEYGUARD until we are also neither on nor over a
- // locked-ish scene.
+ // keyguardish scene.
if (onShade || onQuickSettings || overShade || overlaidShade || overlaidQuickSettings) {
newState = StatusBarState.SHADE_LOCKED;
} else {
@@ -751,7 +754,7 @@ public class StatusBarStateControllerImpl implements
newState = StatusBarState.SHADE;
} else if (onShade || onQuickSettings) {
// We get here if deviceUnlockStatus.isUnlocked is false but we are no longer on or over
- // a locked-ish scene; we want to return SHADE_LOCKED until isUnlocked is also true.
+ // a keyguardish scene; we want to return SHADE_LOCKED until isUnlocked is also true.
newState = StatusBarState.SHADE_LOCKED;
} else {
throw new IllegalArgumentException(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 5f4f72f293a6..0474344ee390 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -594,7 +594,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
private final ColorExtractor.OnColorsChangedListener mOnColorsChangedListener =
(extractor, which) -> updateTheme();
private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor;
- private final GlanceableHubContainerController mGlanceableHubContainerController;
+
+ // Only use before the scene container. Null if scene container is enabled.
+ @Nullable private final GlanceableHubContainerController mGlanceableHubContainerController;
private final EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
@@ -807,7 +809,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
mFingerprintManager = fingerprintManager;
mActivityStarter = activityStarter;
mBrightnessMirrorShowingInteractor = brightnessMirrorShowingInteractor;
- mGlanceableHubContainerController = glanceableHubContainerController;
+ if (!SceneContainerFlag.isEnabled()) {
+ mGlanceableHubContainerController = glanceableHubContainerController;
+ } else {
+ mGlanceableHubContainerController = null;
+ }
mEmergencyGestureIntentFactory = emergencyGestureIntentFactory;
mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
@@ -2972,7 +2978,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
@Override
public void handleCommunalHubTouch(MotionEvent event) {
- mGlanceableHubContainerController.onTouchEvent(event);
+ if (mGlanceableHubContainerController != null) {
+ mGlanceableHubContainerController.onTouchEvent(event);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index b1754fd59cee..200f0804e42b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -34,11 +34,15 @@ import android.view.ViewParent;
import androidx.annotation.Nullable;
+import com.android.compose.animation.scene.ObservableTransitionState;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.ActionClickLogger;
import com.android.systemui.statusbar.CommandQueue;
@@ -52,6 +56,9 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.kotlin.JavaAdapter;
+
+import dagger.Lazy;
import java.util.concurrent.Executor;
@@ -80,6 +87,8 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
private final ActionClickLogger mActionClickLogger;
private int mDisabled2;
protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver();
+ private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+ private final Lazy<SceneInteractor> mSceneInteractorLazy;
/**
*/
@@ -95,7 +104,10 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
ShadeController shadeController,
CommandQueue commandQueue,
ActionClickLogger clickLogger,
- @Main Executor executor) {
+ @Main Executor executor,
+ Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
+ Lazy<SceneInteractor> sceneInteractorLazy,
+ JavaAdapter javaAdapter) {
mContext = context;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mShadeController = shadeController;
@@ -113,20 +125,28 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
mActionClickLogger = clickLogger;
mActivityIntentHelper = new ActivityIntentHelper(mContext);
mGroupExpansionManager = groupExpansionManager;
+ mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy;
+ mSceneInteractorLazy = sceneInteractorLazy;
+
+ if (SceneContainerFlag.isEnabled()) {
+ javaAdapter.alwaysCollectFlow(
+ mDeviceUnlockedInteractorLazy.get().getDeviceUnlockStatus(),
+ deviceUnlockStatus -> onStateChanged(mStatusBarStateController.getState()));
+ javaAdapter.alwaysCollectFlow(
+ mSceneInteractorLazy.get().getTransitionState(),
+ deviceUnlockStatus -> onStateChanged(mStatusBarStateController.getState()));
+ }
}
@Override
public void onStateChanged(int state) {
- boolean hasPendingRemoteInput = mPendingRemoteInputView != null;
- if (state == StatusBarState.SHADE
- && (mStatusBarStateController.leaveOpenOnKeyguardHide() || hasPendingRemoteInput)) {
- if (!mStatusBarStateController.isKeyguardRequested()
- && mKeyguardStateController.isUnlocked()) {
- if (hasPendingRemoteInput) {
- mExecutor.execute(mPendingRemoteInputView::callOnClick);
- }
- mPendingRemoteInputView = null;
- }
+ if (mPendingRemoteInputView == null) {
+ return;
+ }
+
+ if (state == StatusBarState.SHADE && canRetryPendingRemoteInput()) {
+ mExecutor.execute(mPendingRemoteInputView::callOnClick);
+ mPendingRemoteInputView = null;
}
}
@@ -320,6 +340,23 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks,
}
}
+ /**
+ * Returns {@code true} if it is safe to retry a pending remote input. The exact criteria for
+ * this vary depending whether the scene container is enabled.
+ */
+ private boolean canRetryPendingRemoteInput() {
+ if (SceneContainerFlag.isEnabled()) {
+ final boolean isUnlocked = mDeviceUnlockedInteractorLazy.get()
+ .getDeviceUnlockStatus().getValue().isUnlocked();
+ final boolean isIdle = mSceneInteractorLazy.get()
+ .getTransitionState().getValue() instanceof ObservableTransitionState.Idle;
+ return isUnlocked && isIdle;
+ } else {
+ return mKeyguardStateController.isUnlocked()
+ && !mStatusBarStateController.isKeyguardRequested();
+ }
+ }
+
protected class ChallengeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
index da928a364984..3cf206643207 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
@@ -32,6 +32,7 @@ import android.util.SparseBooleanArray;
import androidx.annotation.NonNull;
import com.android.internal.camera.flags.Flags;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.util.ListenerSet;
import java.util.Set;
@@ -41,14 +42,17 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr
private static final int[] SENSORS = new int[] {CAMERA, MICROPHONE};
private final @NonNull SensorPrivacyManager mSensorPrivacyManager;
+ private final @NonNull UserTracker mUserTracker;
private final SparseBooleanArray mSoftwareToggleState = new SparseBooleanArray();
private final SparseBooleanArray mHardwareToggleState = new SparseBooleanArray();
private Boolean mRequiresAuthentication;
private final ListenerSet<Callback> mCallbacks = new ListenerSet<>();
public IndividualSensorPrivacyControllerImpl(
- @NonNull SensorPrivacyManager sensorPrivacyManager) {
+ @NonNull SensorPrivacyManager sensorPrivacyManager,
+ @NonNull UserTracker userTracker) {
mSensorPrivacyManager = sensorPrivacyManager;
+ mUserTracker = userTracker;
}
@Override
@@ -94,12 +98,14 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr
@Override
public void setSensorBlocked(@Source int source, @Sensor int sensor, boolean blocked) {
- mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked);
+ mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked,
+ mUserTracker.getUserId());
}
@Override
public void suppressSensorPrivacyReminders(int sensor, boolean suppress) {
- mSensorPrivacyManager.suppressSensorPrivacyReminders(sensor, suppress);
+ mSensorPrivacyManager.suppressSensorPrivacyReminders(sensor, suppress,
+ mUserTracker.getUserId());
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 7c055c8876ae..7f90242fee2e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -135,3 +135,15 @@ fun <T1, T2, T3, T4, T5, R> combineFlows(
): Flow<R> {
return combine(flow, flow2, flow3, flow4, flow5, transform)
}
+
+fun <T1, T2, T3, T4, T5, T6, R> combineFlows(
+ flow: Flow<T1>,
+ flow2: Flow<T2>,
+ flow3: Flow<T3>,
+ flow4: Flow<T4>,
+ flow5: Flow<T5>,
+ flow6: Flow<T6>,
+ transform: (T1, T2, T3, T4, T5, T6) -> R,
+): Flow<R> {
+ return combine(flow, flow2, flow3, flow4, flow5, flow6, transform)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 079c72f049a6..1f92bc1df9c8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -37,11 +37,8 @@ import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.IAudioService;
import android.media.IVolumeController;
-import android.media.MediaRoute2Info;
import android.media.MediaRouter2Manager;
-import android.media.RoutingSessionInfo;
import android.media.VolumePolicy;
-import android.media.session.MediaController;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.MediaSession.Token;
import android.net.Uri;
@@ -88,7 +85,6 @@ import dalvik.annotation.optimization.NeverCompile;
import java.io.PrintWriter;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@@ -217,7 +213,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
VolumeDialogControllerImpl.class.getSimpleName());
mWorker = new W(mWorkerLooper);
mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
- mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext);
+ mMediaSessionsCallbacksW = new MediaSessionsCallbacks();
mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW);
mAudioSharingInteractor = audioSharingInteractor;
mJavaAdapter = javaAdapter;
@@ -1360,16 +1356,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>();
private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX;
- private final boolean mVolumeAdjustmentForRemoteGroupSessions;
-
- public MediaSessionsCallbacks(Context context) {
- mVolumeAdjustmentForRemoteGroupSessions = context.getResources().getBoolean(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
- }
@Override
public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) {
- if (showForSession(token)) {
addStream(token, "onRemoteUpdate");
int stream = 0;
@@ -1396,12 +1385,10 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level + " of " + ss.levelMax);
mCallbacks.onStateChanged(mState);
}
- }
}
@Override
public void onRemoteVolumeChanged(Token token, int flags) {
- if (showForSession(token)) {
addStream(token, "onRemoteVolumeChanged");
int stream = 0;
synchronized (mRemoteStreams) {
@@ -1420,27 +1407,27 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
if (showUI) {
onShowRequestedW(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED);
}
- }
}
@Override
public void onRemoteRemoved(Token token) {
- if (showForSession(token)) {
- int stream = 0;
- synchronized (mRemoteStreams) {
- if (!mRemoteStreams.containsKey(token)) {
- Log.d(TAG, "onRemoteRemoved: stream doesn't exist, "
- + "aborting remote removed for token:" + token.toString());
- return;
- }
- stream = mRemoteStreams.get(token);
- }
- mState.states.remove(stream);
- if (mState.activeStream == stream) {
- updateActiveStreamW(-1);
+ int stream;
+ synchronized (mRemoteStreams) {
+ if (!mRemoteStreams.containsKey(token)) {
+ Log.d(
+ TAG,
+ "onRemoteRemoved: stream doesn't exist, "
+ + "aborting remote removed for token:"
+ + token.toString());
+ return;
}
- mCallbacks.onStateChanged(mState);
+ stream = mRemoteStreams.get(token);
+ }
+ mState.states.remove(stream);
+ if (mState.activeStream == stream) {
+ updateActiveStreamW(-1);
}
+ mCallbacks.onStateChanged(mState);
}
public void setStreamVolume(int stream, int level) {
@@ -1449,39 +1436,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
Log.w(TAG, "setStreamVolume: No token found for stream: " + stream);
return;
}
- if (showForSession(token)) {
- mMediaSessions.setVolume(token, level);
- }
- }
-
- private boolean showForSession(Token token) {
- if (mVolumeAdjustmentForRemoteGroupSessions) {
- if (DEBUG) {
- Log.d(TAG, "Volume adjustment for remote group sessions allowed,"
- + " showForSession: true");
- }
- return true;
- }
- MediaController ctr = new MediaController(mContext, token);
- String packageName = ctr.getPackageName();
- List<RoutingSessionInfo> sessions =
- mRouter2Manager.getRoutingSessions(packageName);
- if (DEBUG) {
- Log.d(TAG, "Found " + sessions.size() + " routing sessions for package name "
- + packageName);
- }
- for (RoutingSessionInfo session : sessions) {
- if (DEBUG) {
- Log.d(TAG, "Found routingSessionInfo: " + session);
- }
- if (!session.isSystemSession()
- && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
- return true;
- }
- }
-
- Log.d(TAG, "No routing session for " + packageName);
- return false;
+ mMediaSessions.setVolume(token, level);
}
private Token findToken(int stream) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index 5fc19711a26c..8075d117a58a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -104,6 +104,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
@Mock
private Animator mAnimator;
+ @Mock
+ private Animator mEndAnimator;
private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor =
ArgumentCaptor.forClass(Animator.AnimatorListener.class);
@@ -123,7 +125,7 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
MockitoAnnotations.initMocks(this);
when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
- when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+ when(mClipboardOverlayView.getExitAnimation()).thenReturn(mEndAnimator);
when(mClipboardOverlayView.getFadeOutAnimation()).thenReturn(mAnimator);
when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
getImeInsets(new Rect(0, 0, 0, 0)));
@@ -318,11 +320,11 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
mOverlayController.setClipData(mSampleClipData, "");
mCallbacks.onShareButtonTapped();
- verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
- mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator);
+ verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+ mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "");
- verify(mClipboardOverlayView, times(1)).getFadeOutAnimation();
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
}
@Test
@@ -343,8 +345,8 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
initController();
mCallbacks.onDismissButtonTapped();
- verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
- mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator);
+ verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+ mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
// package name is null since we haven't actually set a source for this test
verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, null);
@@ -403,14 +405,18 @@ public class ClipboardOverlayControllerTest extends SysuiTestCase {
mOverlayController.setClipData(mSampleClipData, "first.package");
mCallbacks.onShareButtonTapped();
+ verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+ mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
mOverlayController.setClipData(mSampleClipData, "second.package");
mCallbacks.onShareButtonTapped();
+ verify(mEndAnimator, times(2)).addListener(mAnimatorListenerCaptor.capture());
+ mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
- verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package");
- verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package");
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "first.package");
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package");
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "second.package");
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package");
verifyNoMoreInteractions(mUiEventLogger);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
index ad2550255e29..7d5a334b45ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
@@ -148,6 +148,31 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() {
)
}
+ @Test
+ fun migrate3To4_addSpanYColumn_defaultValuePopulated() {
+ val databaseV3 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 3)
+
+ val fakeWidgetsV3 =
+ listOf(
+ FakeCommunalWidgetItemV3(1, "test_widget_1", 11, 0),
+ FakeCommunalWidgetItemV3(2, "test_widget_2", 12, 10),
+ FakeCommunalWidgetItemV3(3, "test_widget_3", 13, 0),
+ )
+ databaseV3.insertWidgetsV3(fakeWidgetsV3)
+
+ databaseV3.verifyWidgetsV3(fakeWidgetsV3)
+
+ val databaseV4 =
+ migrationTestHelper.runMigrationsAndValidate(
+ name = DATABASE_NAME,
+ version = 4,
+ validateDroppedTables = false,
+ CommunalDatabase.MIGRATION_3_4,
+ )
+
+ databaseV4.verifyWidgetsV4(fakeWidgetsV3.map { it.getV4() })
+ }
+
private fun SupportSQLiteDatabase.insertWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) {
widgets.forEach { widget ->
execSQL(
@@ -157,6 +182,22 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() {
}
}
+ private fun SupportSQLiteDatabase.insertWidgetsV3(widgets: List<FakeCommunalWidgetItemV3>) {
+ widgets.forEach { widget ->
+ execSQL(
+ "INSERT INTO communal_widget_table(" +
+ "widget_id, " +
+ "component_name, " +
+ "item_id, " +
+ "user_serial_number) " +
+ "VALUES(${widget.widgetId}, " +
+ "'${widget.componentName}', " +
+ "${widget.itemId}, " +
+ "${widget.userSerialNumber})"
+ )
+ }
+ }
+
private fun SupportSQLiteDatabase.verifyWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) {
val cursor = query("SELECT * FROM communal_widget_table")
assertThat(cursor.moveToFirst()).isTrue()
@@ -193,6 +234,42 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() {
assertThat(cursor.isAfterLast).isTrue()
}
+ private fun SupportSQLiteDatabase.verifyWidgetsV3(widgets: List<FakeCommunalWidgetItemV3>) {
+ val cursor = query("SELECT * FROM communal_widget_table")
+ assertThat(cursor.moveToFirst()).isTrue()
+
+ widgets.forEach { widget ->
+ assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId)
+ assertThat(cursor.getString(cursor.getColumnIndex("component_name")))
+ .isEqualTo(widget.componentName)
+ assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId)
+ assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number")))
+ .isEqualTo(widget.userSerialNumber)
+
+ cursor.moveToNext()
+ }
+ assertThat(cursor.isAfterLast).isTrue()
+ }
+
+ private fun SupportSQLiteDatabase.verifyWidgetsV4(widgets: List<FakeCommunalWidgetItemV4>) {
+ val cursor = query("SELECT * FROM communal_widget_table")
+ assertThat(cursor.moveToFirst()).isTrue()
+
+ widgets.forEach { widget ->
+ assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId)
+ assertThat(cursor.getString(cursor.getColumnIndex("component_name")))
+ .isEqualTo(widget.componentName)
+ assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId)
+ assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number")))
+ .isEqualTo(widget.userSerialNumber)
+ assertThat(cursor.getInt(cursor.getColumnIndex("span_y"))).isEqualTo(widget.spanY)
+
+ cursor.moveToNext()
+ }
+
+ assertThat(cursor.isAfterLast).isTrue()
+ }
+
private fun SupportSQLiteDatabase.insertRanks(ranks: List<FakeCommunalItemRank>) {
ranks.forEach { rank ->
execSQL("INSERT INTO communal_item_rank_table(rank) VALUES(${rank.rank})")
@@ -238,10 +315,27 @@ class CommunalDatabaseMigrationsTest : SysuiTestCase() {
val userSerialNumber: Int,
)
- private data class FakeCommunalItemRank(
- val rank: Int,
+ private fun FakeCommunalWidgetItemV3.getV4(): FakeCommunalWidgetItemV4 {
+ return FakeCommunalWidgetItemV4(widgetId, componentName, itemId, userSerialNumber, 3)
+ }
+
+ private data class FakeCommunalWidgetItemV3(
+ val widgetId: Int,
+ val componentName: String,
+ val itemId: Int,
+ val userSerialNumber: Int,
+ )
+
+ private data class FakeCommunalWidgetItemV4(
+ val widgetId: Int,
+ val componentName: String,
+ val itemId: Int,
+ val userSerialNumber: Int,
+ val spanY: Int,
)
+ private data class FakeCommunalItemRank(val rank: Int)
+
companion object {
private const val DATABASE_NAME = "communal_db"
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index 15705fbfec33..0bea56007a01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -257,6 +257,58 @@ class TakeScreenshotExecutorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_fromOverview_honorsDisplay() =
+ testScope.runTest {
+ val displayId = 1
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = displayId))
+ val onSaved = { _: Uri? -> }
+ screenshotExecutor.executeScreenshots(
+ createScreenshotRequest(
+ displayId = displayId,
+ source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW,
+ ),
+ onSaved,
+ callback,
+ )
+
+ val dataCaptor = ArgumentCaptor<ScreenshotData>()
+
+ verify(controller).handleScreenshot(dataCaptor.capture(), any(), any())
+
+ assertThat(dataCaptor.value.displayId).isEqualTo(displayId)
+
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_fromOverviewInvalidDisplay_usesDefault() =
+ testScope.runTest {
+ setDisplays(
+ display(TYPE_INTERNAL, id = Display.DEFAULT_DISPLAY),
+ display(TYPE_EXTERNAL, id = 1),
+ )
+ val onSaved = { _: Uri? -> }
+ screenshotExecutor.executeScreenshots(
+ createScreenshotRequest(
+ displayId = 5,
+ source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW,
+ ),
+ onSaved,
+ callback,
+ )
+
+ val dataCaptor = ArgumentCaptor<ScreenshotData>()
+
+ verify(controller).handleScreenshot(dataCaptor.capture(), any(), any())
+
+ assertThat(dataCaptor.value.displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
fun onDestroy_propagatedToControllers() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -527,9 +579,14 @@ class TakeScreenshotExecutorTest : SysuiTestCase() {
runCurrent()
}
- private fun createScreenshotRequest(type: Int = WindowManager.TAKE_SCREENSHOT_FULLSCREEN) =
- ScreenshotRequest.Builder(type, WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER)
+ private fun createScreenshotRequest(
+ type: Int = WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+ source: Int = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER,
+ displayId: Int = Display.DEFAULT_DISPLAY,
+ ) =
+ ScreenshotRequest.Builder(type, source)
.setTopComponent(topComponent)
+ .setDisplayId(displayId)
.also {
if (type == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
it.setBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
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
new file mode 100644
index 000000000000..040a9e959094
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shared.clocks.FontTextStyle
+import com.android.systemui.shared.clocks.LogUtil
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SimpleDigitalClockTextViewTest : SysuiTestCase() {
+ private val messageBuffer = LogUtil.DEBUG_MESSAGE_BUFFER
+ private lateinit var underTest: SimpleDigitalClockTextView
+ private val defaultLargeClockTextSize = 500F
+ private val smallerTextSize = 300F
+ private val largerTextSize = 800F
+ private val firstMeasureTextSize = 100F
+
+ @Before
+ fun setup() {
+ underTest = SimpleDigitalClockTextView(context, messageBuffer)
+ underTest.textStyle = FontTextStyle()
+ underTest.aodStyle = FontTextStyle()
+ underTest.text = "0"
+ underTest.applyTextSize(defaultLargeClockTextSize)
+ }
+
+ @Test
+ fun applySmallerConstrainedTextSize_applyConstrainedTextSize() {
+ underTest.applyTextSize(smallerTextSize, constrainedByHeight = true)
+ assertEquals(smallerTextSize, underTest.textSize * underTest.fontSizeAdjustFactor)
+ }
+
+ @Test
+ fun applyLargerConstrainedTextSize_applyUnconstrainedTextSize() {
+ underTest.applyTextSize(largerTextSize, constrainedByHeight = true)
+ assertEquals(defaultLargeClockTextSize, underTest.textSize)
+ }
+
+ @Test
+ fun applyFirstMeasureConstrainedTextSize_getConstrainedTextSize() {
+ underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+ underTest.applyTextSize(smallerTextSize, constrainedByHeight = true)
+ assertEquals(smallerTextSize, underTest.textSize * underTest.fontSizeAdjustFactor)
+ }
+
+ @Test
+ fun applySmallFirstMeasureConstrainedSizeAndLargerConstrainedTextSize_applyDefaultSize() {
+ underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+ underTest.applyTextSize(largerTextSize, constrainedByHeight = true)
+ assertEquals(defaultLargeClockTextSize, underTest.textSize)
+ }
+
+ @Test
+ fun applyFirstMeasureConstrainedTextSize_applyUnconstrainedTextSize() {
+ underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+ underTest.applyTextSize(defaultLargeClockTextSize)
+ assertEquals(defaultLargeClockTextSize, underTest.textSize)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index c5238194d0a4..81c40dc1778e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -36,7 +36,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.ActionClickLogger;
@@ -50,8 +52,11 @@ import com.android.systemui.statusbar.notification.row.NotificationContentView;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.time.FakeSystemClock;
+import dagger.Lazy;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -71,8 +76,14 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
@Mock private SysuiStatusBarStateController mStatusBarStateController;
@Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Mock private ActivityStarter mActivityStarter;
+ @Mock private Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+ @Mock private Lazy<SceneInteractor> mSceneInteractorLazy;
+ @Mock private JavaAdapter mJavaAdapter;
private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+ @Mock private DeviceUnlockedInteractor mDeviceUnlockedInteractor;
+ @Mock private SceneInteractor mSceneInteractor;
+
private int mCurrentUserId = 0;
private StatusBarRemoteInputCallback mRemoteInputCallback;
@@ -90,7 +101,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase {
mKeyguardStateController, mStatusBarStateController, mStatusBarKeyguardViewManager,
mActivityStarter, mShadeController,
new CommandQueue(mContext, new FakeDisplayTracker(mContext)),
- mock(ActionClickLogger.class), mFakeExecutor));
+ mock(ActionClickLogger.class), mFakeExecutor, mDeviceUnlockedInteractorLazy,
+ mSceneInteractorLazy, mJavaAdapter));
mRemoteInputCallback.mChallengeReceiver = mRemoteInputCallback.new ChallengeReceiver();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index f62beeb16ae5..beba0f0c6286 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -133,10 +133,6 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase {
when(mRingerModeInternalLiveData.getValue()).thenReturn(-1);
when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
when(mUserTracker.getUserContext()).thenReturn(mContext);
- // Enable group volume adjustments
- mContext.getOrCreateTestableResources().addOverride(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions,
- true);
mCallback = mock(VolumeDialogControllerImpl.C.class);
mThreadFactory.setLooper(TestableLooper.get(this).getLooper());
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index 5d7e7c726c6c..1302faaf82ca 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -31,7 +31,7 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) :
provider: ComponentName,
user: UserHandle,
rank: Int?,
- configurator: WidgetConfigurator?
+ configurator: WidgetConfigurator?,
) {
coroutineScope.launch {
val id = nextWidgetId++
@@ -93,6 +93,22 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) :
_communalWidgets.value = fakeDatabase.values.toList()
}
+ override fun updateWidgetSpanY(widgetId: Int, spanY: Int) {
+ coroutineScope.launch {
+ fakeDatabase[widgetId]?.let { widget ->
+ when (widget) {
+ is CommunalWidgetContentModel.Available -> {
+ fakeDatabase[widgetId] = widget.copy(spanY = spanY)
+ }
+ is CommunalWidgetContentModel.Pending -> {
+ fakeDatabase[widgetId] = widget.copy(spanY = spanY)
+ }
+ }
+ _communalWidgets.value = fakeDatabase.values.toList()
+ }
+ }
+ }
+
override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) {}
override fun abortRestoreWidgets() {}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
index b37cac1d36fd..ba316835f2b3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
@@ -19,8 +19,10 @@ package com.android.systemui.keyboard.data.repository
import com.android.systemui.keyboard.data.model.Keyboard
import com.android.systemui.keyboard.shared.model.BacklightModel
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.filterNotNull
class FakeKeyboardRepository : KeyboardRepository {
@@ -32,8 +34,14 @@ class FakeKeyboardRepository : KeyboardRepository {
// filtering to make sure backlight doesn't have default initial value
override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull()
- private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
- override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
+ // implemented as channel because original implementation is modeling events: it doesn't hold
+ // state so it won't always emit once connected. And it's bad if some tests depend on that
+ // incorrect behaviour.
+ private val _newlyConnectedKeyboard: Channel<Keyboard> = Channel()
+ override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.consumeAsFlow()
+
+ private val _connectedKeyboards: MutableStateFlow<Set<Keyboard>> = MutableStateFlow(setOf())
+ override val connectedKeyboards: Flow<Set<Keyboard>> = _connectedKeyboards
fun setBacklight(state: BacklightModel) {
_backlightState.value = state
@@ -43,7 +51,14 @@ class FakeKeyboardRepository : KeyboardRepository {
_isAnyKeyboardConnected.value = connected
}
+ fun setConnectedKeyboards(keyboards: Set<Keyboard>) {
+ _connectedKeyboards.value = keyboards
+ _isAnyKeyboardConnected.value = keyboards.isNotEmpty()
+ }
+
fun setNewlyConnectedKeyboard(keyboard: Keyboard) {
- _newlyConnectedKeyboard.value = keyboard
+ _newlyConnectedKeyboard.trySend(keyboard)
+ _connectedKeyboards.value += keyboard
+ _isAnyKeyboardConnected.value = true
}
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index c87d516d2ab4..ab9cc20763a8 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -205,6 +205,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
.verifyCallerCanExecuteAppFunction(
callingUid,
callingPid,
+ targetUser,
requestInternal.getCallingPackage(),
targetPackageName,
requestInternal.getClientRequest().getFunctionIdentifier())
diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
index 3592ed587ab0..5393b939b5ed 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
@@ -64,6 +64,9 @@ public interface CallerValidator {
* {@link Manifest.permission#EXECUTE_APP_FUNCTIONS} granted. In some cases, app functions can
* still opt-out of caller having {@link Manifest.permission#EXECUTE_APP_FUNCTIONS}.
*
+ * @param callingUid The calling uid.
+ * @param callingPid The calling pid.
+ * @param targetUser The user which the caller is requesting to execute as.
* @param callerPackageName The calling package (as previously validated).
* @param targetPackageName The package that owns the app function to execute.
* @param functionId The id of the app function to execute.
@@ -72,6 +75,7 @@ public interface CallerValidator {
AndroidFuture<Boolean> verifyCallerCanExecuteAppFunction(
int callingUid,
int callingPid,
+ @NonNull UserHandle targetUser,
@NonNull String callerPackageName,
@NonNull String targetPackageName,
@NonNull String functionId);
diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
index 8b6251a59e3a..e85a70d5845a 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
@@ -93,6 +93,7 @@ class CallerValidatorImpl implements CallerValidator {
public AndroidFuture<Boolean> verifyCallerCanExecuteAppFunction(
int callingUid,
int callingPid,
+ @NonNull UserHandle targetUser,
@NonNull String callerPackageName,
@NonNull String targetPackageName,
@NonNull String functionId) {
@@ -122,7 +123,10 @@ class CallerValidatorImpl implements CallerValidator {
FutureAppSearchSession futureAppSearchSession =
new FutureAppSearchSessionImpl(
- mContext.getSystemService(AppSearchManager.class),
+ Objects.requireNonNull(
+ mContext
+ .createContextAsUser(targetUser, 0)
+ .getSystemService(AppSearchManager.class)),
THREAD_POOL_EXECUTOR,
new SearchContext.Builder(APP_FUNCTION_STATIC_METADATA_DB).build());
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 0a9109b3e98c..d752429e64f7 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -16,7 +16,6 @@
package com.android.server.media;
-import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
import static android.media.VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
import static android.media.VolumeProvider.VOLUME_CONTROL_FIXED;
import static android.media.VolumeProvider.VOLUME_CONTROL_RELATIVE;
@@ -48,9 +47,7 @@ import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.MediaMetadata;
-import android.media.MediaRouter2Manager;
import android.media.Rating;
-import android.media.RoutingSessionInfo;
import android.media.VolumeProvider;
import android.media.session.ISession;
import android.media.session.ISessionCallback;
@@ -186,7 +183,6 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
private final MediaSessionService mService;
private final UriGrantsManagerInternal mUgmInternal;
private final Context mContext;
- private final boolean mVolumeAdjustmentForRemoteGroupSessions;
private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions;
@@ -311,8 +307,6 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
mAudioAttrs = DEFAULT_ATTRIBUTES;
mPolicies = policies;
mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
- mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
mForegroundServiceDelegationOptions = createForegroundServiceDelegationOptions();
@@ -659,49 +653,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde
}
return false;
}
- if (mVolumeAdjustmentForRemoteGroupSessions) {
- if (DEBUG) {
- Slog.d(
- TAG,
- "Volume adjustment for remote group sessions allowed so MediaSessionRecord"
- + " can handle volume key");
- }
- return true;
- }
- // See b/228021646 for details.
- MediaRouter2Manager mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
- List<RoutingSessionInfo> sessions = mRouter2Manager.getRoutingSessions(mPackageName);
- boolean foundNonSystemSession = false;
- boolean remoteSessionAllowVolumeAdjustment = true;
- if (DEBUG) {
- Slog.d(
- TAG,
- "Found "
- + sessions.size()
- + " routing sessions for package name "
- + mPackageName);
- }
- for (RoutingSessionInfo session : sessions) {
- if (DEBUG) {
- Slog.d(TAG, "Found routingSessionInfo: " + session);
- }
- if (!session.isSystemSession()) {
- foundNonSystemSession = true;
- if (session.getVolumeHandling() == PLAYBACK_VOLUME_FIXED) {
- remoteSessionAllowVolumeAdjustment = false;
- }
- }
- }
- if (!foundNonSystemSession) {
- if (DEBUG) {
- Slog.d(
- TAG,
- "Package " + mPackageName
- + " has a remote media session but no associated routing session");
- }
- }
-
- return foundNonSystemSession && remoteSessionAllowVolumeAdjustment;
+ return true;
}
@Override
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 197f0070b553..e47b4c2ee147 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -373,6 +373,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
//The config value can be overridden using Settings.Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS
static final int DOUBLE_PRESS_PRIMARY_NOTHING = 0;
static final int DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP = 1;
+ static final int DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP = 2;
// Must match: config_triplePressOnStemPrimaryBehavior in config.xml
// The config value can be overridden using Settings.Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS
@@ -1596,6 +1597,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
performStemPrimaryDoublePressSwitchToRecentTask();
}
break;
+ case DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP:
+ final int stemPrimaryKeyDeviceId = INVALID_INPUT_DEVICE_ID;
+ handleKeyGestureInKeyGestureController(
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS,
+ stemPrimaryKeyDeviceId, KEYCODE_STEM_PRIMARY, /* metaState= */ 0);
+ break;
}
}
@@ -7244,6 +7251,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return "DOUBLE_PRESS_PRIMARY_NOTHING";
case DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP:
return "DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP";
+ case DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP:
+ return "DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP";
default:
return Integer.toString(behavior);
}
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 1346a294b7d8..dc482422f9d9 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -1282,11 +1282,9 @@ public final class HintManagerService extends SystemService {
boolean updateHintAllowedByProcState(boolean allowed) {
synchronized (this) {
if (allowed && !mUpdateAllowedByProcState && !mShouldForcePause) {
- Slogf.e(TAG, "ADPF IS GETTING RESUMED? UID: " + mUid + " TAG: " + mTag);
resume();
}
if (!allowed && mUpdateAllowedByProcState) {
- Slogf.e(TAG, "ADPF IS GETTING PAUSED? UID: " + mUid + " TAG: " + mTag);
pause();
}
mUpdateAllowedByProcState = allowed;
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index cbe6700f4d41..6ede334aec80 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -230,6 +230,20 @@ java_library {
}
java_library {
+ name: "servicestests-utils-ravenwood",
+ srcs: [
+ "utils/**/*.java",
+ "utils/**/*.kt",
+ "utils-mockito/**/*.kt",
+ ],
+ libs: [
+ "android.test.runner.stubs.system",
+ "junit",
+ "mockito-ravenwood-prebuilt",
+ ],
+}
+
+java_library {
name: "mockito-test-utils",
srcs: [
"utils-mockito/**/*.kt",
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index cf58d9b49832..c6cc941ba1cd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -70,6 +70,7 @@ import java.util.Set;
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper
@@ -147,6 +148,147 @@ public class ZenModeDiffTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void testRuleDiff_toStringNoChangeAddRemove() throws Exception {
+ // Start with two identical rules
+ ZenModeConfig.ZenRule r1 = makeRule();
+ ZenModeConfig.ZenRule r2 = makeRule();
+
+ ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{no changes}");
+
+ d = new ZenModeDiff.RuleDiff(r1, null);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{removed}");
+
+ d = new ZenModeDiff.RuleDiff(null, r2);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{added}");
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void testRuleDiff_toString() throws Exception {
+ // Start with two identical rules
+ ZenModeConfig.ZenRule r1 = makeRule();
+ ZenModeConfig.ZenRule r2 = makeRule();
+
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenModeConfig.ZenRule.class, getZenRuleExemptFields(), false);
+ generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
+
+ ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{"
+ + "enabled:true->false, "
+ + "conditionOverride:2->1, "
+ + "name:string1->string2, "
+ + "zenMode:2->1, "
+ + "conditionId:null->, "
+ + "condition:null->Condition["
+ + "state=STATE_TRUE,"
+ + "id=hello:,"
+ + "summary=,"
+ + "line1=,"
+ + "line2=,"
+ + "icon=-1,"
+ + "source=SOURCE_UNKNOWN,"
+ + "flags=2], "
+ + "component:null->ComponentInfo{b/b}, "
+ + "configurationActivity:null->ComponentInfo{a/a}, "
+ + "id:string1->string2, "
+ + "creationTime:200->100, "
+ + "enabler:string1->string2, "
+ + "zenPolicy:ZenPolicyDiff{"
+ + "mPriorityCategories_Reminders:1->2, "
+ + "mPriorityCategories_Events:1->2, "
+ + "mPriorityCategories_Messages:1->2, "
+ + "mPriorityCategories_Calls:1->2, "
+ + "mPriorityCategories_RepeatCallers:1->2, "
+ + "mPriorityCategories_Alarms:1->2, "
+ + "mPriorityCategories_Media:1->2, "
+ + "mPriorityCategories_System:1->2, "
+ + "mPriorityCategories_Conversations:1->2, "
+ + "mVisualEffects_FullScreenIntent:1->2, "
+ + "mVisualEffects_Lights:1->2, "
+ + "mVisualEffects_Peek:1->2, "
+ + "mVisualEffects_StatusBar:1->2, "
+ + "mVisualEffects_Badge:1->2, "
+ + "mVisualEffects_Ambient:1->2, "
+ + "mVisualEffects_NotificationList:1->2, "
+ + "mPriorityMessages:2->1, "
+ + "mPriorityCalls:2->1, "
+ + "mConversationSenders:2->1, "
+ + "mAllowChannels:2->1}, "
+ + "modified:true->false, "
+ + "pkg:string1->string2, "
+ + "zenDeviceEffects:ZenDeviceEffectsDiff{"
+ + "mGrayscale:true->false, "
+ + "mSuppressAmbientDisplay:true->false, "
+ + "mDimWallpaper:true->false, "
+ + "mNightMode:true->false, "
+ + "mDisableAutoBrightness:true->false, "
+ + "mDisableTapToWake:true->false, "
+ + "mDisableTiltToWake:true->false, "
+ + "mDisableTouch:true->false, "
+ + "mMinimizeRadioUsage:true->false, "
+ + "mMaximizeDoze:true->false, "
+ + "mExtraEffects:[effect1]->[effect2]}, "
+ + "triggerDescription:string1->string2, "
+ + "type:2->1, "
+ + "allowManualInvocation:true->false, "
+ + "iconResName:string1->string2, "
+ + "legacySuppressedEffects:2->1}");
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void testRuleDiff_toStringNullStartPolicy() throws Exception {
+ // Start with two identical rules
+ ZenModeConfig.ZenRule r1 = makeRule();
+ ZenModeConfig.ZenRule r2 = makeRule();
+
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenModeConfig.ZenRule.class, getZenRuleExemptFields(), false);
+ generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
+
+ // Create a ZenRule with ZenDeviceEffects and ZenPolicy as null.
+ r1.zenPolicy = null;
+ r1.zenDeviceEffects = null;
+ ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{"
+ + "enabled:true->false, "
+ + "conditionOverride:2->1, "
+ + "name:string1->string2, "
+ + "zenMode:2->1, "
+ + "conditionId:null->, "
+ + "condition:null->Condition["
+ + "state=STATE_TRUE,"
+ + "id=hello:,"
+ + "summary=,"
+ + "line1=,"
+ + "line2=,"
+ + "icon=-1,"
+ + "source=SOURCE_UNKNOWN,"
+ + "flags=2], "
+ + "component:null->ComponentInfo{b/b}, "
+ + "configurationActivity:null->ComponentInfo{a/a}, "
+ + "id:string1->string2, "
+ + "creationTime:200->100, "
+ + "enabler:string1->string2, "
+ + "zenPolicy:ZenPolicyDiff{added}, "
+ + "modified:true->false, "
+ + "pkg:string1->string2, "
+ + "zenDeviceEffects:ZenDeviceEffectsDiff{added}, "
+ + "triggerDescription:string1->string2, "
+ + "type:2->1, "
+ + "allowManualInvocation:true->false, "
+ + "iconResName:string1->string2, "
+ + "legacySuppressedEffects:2->1}");
+ }
+
+ @Test
public void testDeviceEffectsDiff_addRemoveSame() {
// Test add, remove, and both sides same
ZenDeviceEffects effects = new ZenDeviceEffects.Builder().build();
@@ -216,12 +358,18 @@ public class ZenModeDiffTest extends UiServiceTestCase {
generateFieldDiffs(effects1, effects2, fieldsForDiff, expectedFrom, expectedTo);
d = new ZenModeDiff.DeviceEffectsDiff(effects1, effects2);
- assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{mNightMode:true->false, "
- + "mDisableTapToWake:true->false, mDisableAutoBrightness:true->false, "
- + "mSuppressAmbientDisplay:true->false, mDisableTiltToWake:true->false, "
- + "mGrayscale:true->false, mDisableTouch:true->false, mMaximizeDoze:true->false, "
- + "mMinimizeRadioUsage:true->false, mExtraEffects:null->[], "
- + "mDimWallpaper:true->false}");
+ assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{"
+ + "mGrayscale:true->false, "
+ + "mSuppressAmbientDisplay:true->false, "
+ + "mDimWallpaper:true->false, "
+ + "mNightMode:true->false, "
+ + "mDisableAutoBrightness:true->false, "
+ + "mDisableTapToWake:true->false, "
+ + "mDisableTiltToWake:true->false, "
+ + "mDisableTouch:true->false, "
+ + "mMinimizeRadioUsage:true->false, "
+ + "mMaximizeDoze:true->false, "
+ + "mExtraEffects:[effect1]->[effect2]}");
}
@@ -307,17 +455,27 @@ public class ZenModeDiffTest extends UiServiceTestCase {
generateFieldDiffsForZenPolicy(policy1, policy2, fieldsForDiff, expectedFrom, expectedTo);
d = new ZenModeDiff.PolicyDiff(policy1, policy2);
- assertThat(d.toString()).isEqualTo("ZenPolicyDiff{mPriorityCalls:2->1, "
- + "mVisualEffects_StatusBar:1->2, mPriorityCategories_RepeatCallers:1->2, "
- + "mPriorityCategories_Calls:1->2, mPriorityCategories_Media:1->2, "
- + "mConversationSenders:2->1, mPriorityCategories_Reminders:1->2, "
- + "mVisualEffects_Badge:1->2, mPriorityCategories_Messages:1->2, "
- + "mAllowChannels:2->1, mPriorityMessages:2->1, "
- + "mVisualEffects_NotificationList:1->2, mVisualEffects_FullScreenIntent:1->2, "
- + "mPriorityCategories_Alarms:1->2, mVisualEffects_Lights:1->2, "
- + "mPriorityCategories_Events:1->2, mVisualEffects_Ambient:1->2, "
- + "mPriorityCategories_System:1->2, mPriorityCategories_Conversations:1->2, "
- + "mVisualEffects_Peek:1->2}");
+ assertThat(d.toString()).isEqualTo("ZenPolicyDiff{"
+ + "mPriorityCategories_Reminders:1->2, "
+ + "mPriorityCategories_Events:1->2, "
+ + "mPriorityCategories_Messages:1->2, "
+ + "mPriorityCategories_Calls:1->2, "
+ + "mPriorityCategories_RepeatCallers:1->2, "
+ + "mPriorityCategories_Alarms:1->2, "
+ + "mPriorityCategories_Media:1->2, "
+ + "mPriorityCategories_System:1->2, "
+ + "mPriorityCategories_Conversations:1->2, "
+ + "mVisualEffects_FullScreenIntent:1->2, "
+ + "mVisualEffects_Lights:1->2, "
+ + "mVisualEffects_Peek:1->2, "
+ + "mVisualEffects_StatusBar:1->2, "
+ + "mVisualEffects_Badge:1->2, "
+ + "mVisualEffects_Ambient:1->2, "
+ + "mVisualEffects_NotificationList:1->2, "
+ + "mPriorityMessages:2->1, "
+ + "mPriorityCalls:2->1, "
+ + "mConversationSenders:2->1, "
+ + "mAllowChannels:2->1}");
}
private static Set<String> getZenRuleExemptFields() {
@@ -701,6 +859,44 @@ public class ZenModeDiffTest extends UiServiceTestCase {
expectedA.put(f.getName(), "string1");
f.set(b, "string2");
expectedB.put(f.getName(), "string2");
+ } else if (Set.class.equals(t)) {
+ Set<String> aSet = Set.of("effect1");
+ Set<String> bSet = Set.of("effect2");
+ f.set(a, aSet);
+ expectedA.put(f.getName(), aSet);
+ f.set(b, bSet);
+ expectedB.put(f.getName(), bSet);
+ } else if (ZenDeviceEffects.class.equals(t)) {
+ // Recurse into generating field diffs for ZenDeviceEffects.
+ ZenDeviceEffects effects1 = new ZenDeviceEffects.Builder().build();
+ ZenDeviceEffects effects2 = new ZenDeviceEffects.Builder().build();
+ // maps mapping field name -> expected output value as we set diffs
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenDeviceEffects.class, Collections.emptySet() /*no exempt fields*/, true);
+ generateFieldDiffs(effects1, effects2, fieldsForDiff, expectedFrom, expectedTo);
+ f.set(a, effects1);
+ expectedA.put(f.getName(), effects1);
+ f.set(b, effects2);
+ expectedB.put(f.getName(), effects2);
+ } else if (ZenPolicy.class.equals(t)) {
+ // Recurse into generating field diffs for ZenPolicy.
+ ZenPolicy policy1 = new ZenPolicy.Builder().build();
+ ZenPolicy policy2 = new ZenPolicy.Builder().build();
+ // maps mapping field name -> expected output value as we set diffs
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(ZenPolicy.class,
+ Collections.emptySet(), false);
+ generateFieldDiffsForZenPolicy(policy1, policy2, fieldsForDiff, expectedFrom,
+ expectedTo);
+ f.set(a, policy1);
+ expectedA.put(f.getName(), policy1);
+ f.set(b, policy2);
+ expectedB.put(f.getName(), policy2);
} else {
// catch-all for other types: have the field be "added"
f.set(a, null);
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
index 9b92ff45952b..3ea3235df0f4 100644
--- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -23,6 +23,7 @@ import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS;
import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.server.policy.PhoneWindowManager.DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP;
import static com.android.server.policy.PhoneWindowManager.DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT;
import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS;
@@ -32,6 +33,7 @@ import static com.android.server.policy.PhoneWindowManager.TRIPLE_PRESS_PRIMARY_
import android.app.ActivityManager.RecentTaskInfo;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.content.ComponentName;
+import android.hardware.input.KeyGestureEvent;
import android.os.RemoteException;
import android.provider.Settings;
import android.view.Display;
@@ -236,6 +238,19 @@ public class StemKeyGestureTests extends ShortcutKeyTestBase {
}
@Test
+ public void stemDoubleKey_behaviorIsLaunchFitness_gestureEventFired() {
+ overrideBehavior(
+ STEM_PRIMARY_BUTTON_DOUBLE_PRESS, DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP);
+ setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+
+ sendKey(KEYCODE_STEM_PRIMARY);
+ sendKey(KEYCODE_STEM_PRIMARY);
+
+ mPhoneWindowManager.assertKeyGestureEventSentToKeyGestureController(
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS);
+ }
+
+ @Test
public void stemTripleKey_EarlyShortPress_AllAppsThenBackToOriginalThenToggleA11y()
throws RemoteException {
overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 1aa908792c0e..a85f8666d2e1 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -919,4 +919,9 @@ class TestPhoneWindowManager {
mTestLooper.dispatchAll();
Assert.assertEquals(expectEnabled, mIsTalkBackEnabled);
}
+
+ void assertKeyGestureEventSentToKeyGestureController(int gestureType) {
+ verify(mInputManagerInternal)
+ .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType));
+ }
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index ff302f6b1a65..ff966ae66e0a 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -2298,13 +2298,9 @@ public class TelephonyManager {
*
* See {@link #getImei(int)} for details on the required permissions and behavior
* when the caller does not hold sufficient permissions.
- *
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
*/
@SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236).
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
public String getImei() {
return getImei(getSlotIndex());
}
@@ -2343,13 +2339,9 @@ public class TelephonyManager {
* </ul>
*
* @param slotIndex of which IMEI is returned
- *
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
*/
@SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236).
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
public String getImei(int slotIndex) {
ITelephony telephony = getITelephony();
if (telephony == null) return null;
@@ -2366,11 +2358,7 @@ public class TelephonyManager {
/**
* Returns the Type Allocation Code from the IMEI. Return null if Type Allocation Code is not
* available.
- *
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
*/
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
@Nullable
public String getTypeAllocationCode() {
return getTypeAllocationCode(getSlotIndex());
@@ -2381,11 +2369,7 @@ public class TelephonyManager {
* available.
*
* @param slotIndex of which Type Allocation Code is returned
- *
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
*/
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
@Nullable
public String getTypeAllocationCode(int slotIndex) {
ITelephony telephony = getITelephony();
@@ -19378,12 +19362,9 @@ public class TelephonyManager {
* </ul>
*
* @return Primary IMEI of type string
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
* @throws SecurityException if the caller does not have the required permission/privileges
*/
@NonNull
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
public String getPrimaryImei() {
try {
ITelephony telephony = getITelephony();