diff options
47 files changed, 888 insertions, 258 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 6ff1bfc5291c..e39928b5e091 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -3090,6 +3090,25 @@ public class Notification implements Parcelable } /** + * @hide + */ + public int loadHeaderAppIconRes(Context context) { + ApplicationInfo info = null; + if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) { + info = extras.getParcelable( + EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo.class); + } + if (info == null) { + info = context.getApplicationInfo(); + } + if (info != null) { + return info.icon; + } + return 0; + } + + /** * Removes heavyweight parts of the Notification object for archival or for sending to * listeners when the full contents are not necessary. * @hide @@ -5963,12 +5982,21 @@ public class Notification implements Parcelable } private void bindSmallIcon(RemoteViews contentView, StandardTemplateParams p) { - if (mN.mSmallIcon == null && mN.icon != 0) { + if (Flags.notificationsUseAppIcon()) { + // Override small icon with app icon + mN.mSmallIcon = Icon.createWithResource(mContext, + mN.loadHeaderAppIconRes(mContext)); + } else if (mN.mSmallIcon == null && mN.icon != 0) { mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon); } + contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon); contentView.setInt(R.id.icon, "setImageLevel", mN.iconLevel); - processSmallIconColor(mN.mSmallIcon, contentView, p); + + // Don't change color if we're using the app icon. + if (!Flags.notificationsUseAppIcon()) { + processSmallIconColor(mN.mSmallIcon, contentView, p); + } } /** @@ -6804,7 +6832,8 @@ public class Notification implements Parcelable */ private void processSmallIconColor(Icon smallIcon, RemoteViews contentView, StandardTemplateParams p) { - boolean colorable = !isLegacy() || getColorUtil().isGrayscaleIcon(mContext, smallIcon); + boolean colorable = !isLegacy() || getColorUtil().isGrayscaleIcon(mContext, + smallIcon); int color = getSmallIconColor(p); contentView.setInt(R.id.icon, "setBackgroundColor", getBackgroundColor(p)); diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index ac843cbfbfac..f5e0f684c5db 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -282,3 +282,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "headless_single_user_fixes" + namespace: "enterprise" + description: "Various fixes for headless single user mode" + bug: "289515470" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 250953e61137..28afd5f63435 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -1,5 +1,9 @@ package: "android.app" +# Note: When adding a new flag here, consider including the word "notification(s)" in the flag name +# when appropriate, as it's not currently part of the namespace so it may not be obvious what the +# flag relates to. + flag { name: "modes_api" is_exported: true @@ -41,6 +45,13 @@ flag { } flag { + name: "notifications_use_app_icon" + namespace: "systemui" + description: "Experiment to replace the small icon in a notification with the app icon." + bug: "335211019" +} + +flag { name: "keyguard_private_notifications" namespace: "systemui" description: "Fixes the behavior of KeyguardManager#setPrivateNotificationsAllowed()" diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 4c0da7c98488..f5bff9dc1d0e 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -4844,6 +4844,16 @@ public abstract class PackageManager { public static final String FEATURE_ROTARY_ENCODER_LOW_RES = "android.hardware.rotaryencoder.lowres"; + /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device has + * support for contextual search helper. + * + * @hide + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_CONTEXTUAL_SEARCH_HELPER = + "android.software.contextualsearch"; + /** @hide */ public static final boolean APP_ENUMERATION_ENABLED_BY_DEFAULT = true; diff --git a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java index 770448bd594b..fc72db3c5791 100644 --- a/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java +++ b/core/java/android/hardware/biometrics/BiometricFingerprintConstants.java @@ -221,7 +221,8 @@ public interface BiometricFingerprintConstants { FINGERPRINT_ACQUIRED_IMMOBILE, FINGERPRINT_ACQUIRED_TOO_BRIGHT, FINGERPRINT_ACQUIRED_POWER_PRESSED, - FINGERPRINT_ACQUIRED_RE_ENROLL}) + FINGERPRINT_ACQUIRED_RE_ENROLL_OPTIONAL, + FINGERPRINT_ACQUIRED_RE_ENROLL_FORCED}) @Retention(RetentionPolicy.SOURCE) @interface FingerprintAcquired {} @@ -316,7 +317,13 @@ public interface BiometricFingerprintConstants { * This message is sent to encourage the user to re-enroll their fingerprints. * @hide */ - int FINGERPRINT_ACQUIRED_RE_ENROLL = 12; + int FINGERPRINT_ACQUIRED_RE_ENROLL_OPTIONAL = 12; + + /** + * This message is sent to force the user to re-enroll their fingerprints. + * @hide + */ + int FINGERPRINT_ACQUIRED_RE_ENROLL_FORCED = 13; /** * @hide diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 4c941fda1f14..f31d390d376c 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5188,7 +5188,7 @@ <string name="lock_to_app_unlock_password">Ask for password before unpinning</string> <!-- Notification shown when device owner silently installs a package [CHAR LIMIT=NONE] --> - <string name="package_installed_device_owner">Installed by your admin</string> + <string name="package_installed_device_owner">Installed by your admin.\nGo to settings to view granted permissions</string> <!-- Notification shown when device owner silently updates a package [CHAR LIMIT=NONE] --> <string name="package_updated_device_owner">Updated by your admin</string> <!-- Notification shown when device owner silently deletes a package [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS new file mode 100644 index 000000000000..73a5a23909c5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS @@ -0,0 +1,5 @@ +# Android > Android OS & Apps > Framework (Java + Native) > Window Manager > WM Shell > Freeform +# Bug component: 929241 + +uysalorhan@google.com +pragyabajoria@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt new file mode 100644 index 000000000000..4c781d36acf6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd +import android.tools.flicker.config.AssertionTemplates +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerConfigEntry +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.desktopmode.Components +import android.tools.flicker.extractors.ITransitionMatcher +import android.tools.flicker.extractors.ShellTransitionScenarioExtractor +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import android.tools.traces.wm.Transition +import android.tools.traces.wm.TransitionType +import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class EnterDesktopWithDragLandscape : EnterDesktopWithDrag(Rotation.ROTATION_90) { + @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() = + super.enterDesktopWithDrag() + + companion object { + private val END_DRAG_TO_DESKTOP = FlickerConfigEntry( + scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), + extractor = ShellTransitionScenarioExtractor( + transitionMatcher = object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP} + } + }), + assertions = AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(END_DRAG_TO_DESKTOP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt new file mode 100644 index 000000000000..d99d875fb126 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd +import android.tools.flicker.config.AssertionTemplates +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerConfigEntry +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.desktopmode.Components +import android.tools.flicker.extractors.ITransitionMatcher +import android.tools.flicker.extractors.ShellTransitionScenarioExtractor +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import android.tools.traces.wm.Transition +import android.tools.traces.wm.TransitionType +import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class EnterDesktopWithDragPortrait : EnterDesktopWithDrag(Rotation.ROTATION_0) { + @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() = + super.enterDesktopWithDrag() + + companion object { + private val END_DRAG_TO_DESKTOP = FlickerConfigEntry( + scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), + extractor = ShellTransitionScenarioExtractor( + transitionMatcher = object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP} + } + }), + assertions = AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(END_DRAG_TO_DESKTOP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt new file mode 100644 index 000000000000..0403b4f64faf --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.wm.shell.flicker.service.common.Utils +import com.android.wm.shell.flicker.utils.DesktopModeUtils +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Base Test Class") +abstract class EnterDesktopWithDrag +@JvmOverloads +constructor(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = SimpleAppHelper(instrumentation) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + } + + @Test + open fun enterDesktopWithDrag() { + DesktopModeUtils.enterDesktopWithDrag(wmHelper, device, testApp) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt new file mode 100644 index 000000000000..345bc5ebb20e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.utils + +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.helpers.SYSTEMUI_PACKAGE +import android.tools.traces.component.IComponentMatcher +import android.tools.traces.parsers.WindowManagerStateHelper +import android.tools.traces.wm.WindowingMode +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until + +/** + * Provides a collection of utility functions for desktop mode testing. + */ +object DesktopModeUtils { + private const val TIMEOUT_MS = 3_000L + private const val CAPTION = "desktop_mode_caption" + private const val CAPTION_HANDLE = "caption_handle" + private const val MAXIMIZE_BUTTON = "maximize_button_view" + + private val captionFullscreen: BySelector + get() = By.res(SYSTEMUI_PACKAGE, CAPTION) + private val captionHandle: BySelector + get() = By.res(SYSTEMUI_PACKAGE, CAPTION_HANDLE) + private val maximizeButton: BySelector + get() = By.res(SYSTEMUI_PACKAGE, MAXIMIZE_BUTTON) + + /** + * Wait for an app moved to desktop to finish its transition. + */ + private fun waitForAppToMoveToDesktop( + wmHelper: WindowManagerStateHelper, + currentApp: IComponentMatcher, + ) { + wmHelper + .StateSyncBuilder() + .withWindowSurfaceAppeared(currentApp) + .withFreeformApp(currentApp) + .withAppTransitionIdle() + .waitForAndVerify() + } + + /** + * Click maximise button on the app header for the given app. + */ + fun maximiseDesktopApp( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + currentApp: StandardAppHelper + ) { + if (wmHelper.getWindow(currentApp)?.windowingMode + != WindowingMode.WINDOWING_MODE_FREEFORM.value) + error("expected a freeform window to maximise but window is not in freefrom mode") + + val maximizeButton = + device.wait(Until.findObject(maximizeButton), TIMEOUT_MS) + ?: error("Unable to find view $maximizeButton\n") + maximizeButton.click() + } + + /** + * Move an app to Desktop by dragging the app handle at the top. + */ + fun enterDesktopWithDrag( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + currentApp: StandardAppHelper, + ) { + currentApp.launchViaIntent(wmHelper) + dragToDesktop(wmHelper, currentApp, device) + waitForAppToMoveToDesktop(wmHelper, currentApp) + } + + private fun dragToDesktop( + wmHelper: WindowManagerStateHelper, + currentApp: StandardAppHelper, + device: UiDevice + ) { + val windowRect = wmHelper.getWindowRegion(currentApp).bounds + val startX = windowRect.centerX() + + // Start dragging a little under the top to prevent dragging the notification shade. + val startY = 10 + + val displayRect = + wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect + ?: throw IllegalStateException("Default display is null") + + // The position we want to drag to + val endY = displayRect.centerY() / 2 + + // drag the window to move to desktop + device.drag(startX, startY, startX, endY, 100) + } +} diff --git a/location/Android.bp b/location/Android.bp index eb7cd01111b2..5ba35ac3328a 100644 --- a/location/Android.bp +++ b/location/Android.bp @@ -26,6 +26,7 @@ java_sdk_library { "com.android.internal.location", ], libs: [ + "android.location.flags-aconfig-java", "app-compat-annotations", "unsupportedappusage", // for android.compat.annotation.UnsupportedAppUsage ], diff --git a/location/api/current.txt b/location/api/current.txt index 85e9f65a0718..61afd266aecf 100644 --- a/location/api/current.txt +++ b/location/api/current.txt @@ -412,8 +412,8 @@ package android.location { field public static final int TYPE_GPS_L1CA = 257; // 0x101 field public static final int TYPE_GPS_L2CNAV = 258; // 0x102 field public static final int TYPE_GPS_L5CNAV = 259; // 0x103 - field @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) public static final int TYPE_IRN_L1 = 1795; // 0x703 - field @FlaggedApi(Flags.FLAG_GNSS_API_NAVIC_L1) public static final int TYPE_IRN_L5 = 1794; // 0x702 + field @FlaggedApi("android.location.flags.gnss_api_navic_l1") public static final int TYPE_IRN_L1 = 1795; // 0x703 + field @FlaggedApi("android.location.flags.gnss_api_navic_l1") public static final int TYPE_IRN_L5 = 1794; // 0x702 field public static final int TYPE_IRN_L5CA = 1793; // 0x701 field public static final int TYPE_QZS_L1CA = 1025; // 0x401 field public static final int TYPE_SBS = 513; // 0x201 @@ -682,7 +682,7 @@ package android.location.altitude { public final class AltitudeConverter { ctor public AltitudeConverter(); method @WorkerThread public void addMslAltitudeToLocation(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException; - method @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL) public boolean tryAddMslAltitudeToLocation(@NonNull android.location.Location); + method @FlaggedApi("android.location.flags.geoid_heights_via_altitude_hal") public boolean tryAddMslAltitudeToLocation(@NonNull android.location.Location); } } diff --git a/location/api/system-current.txt b/location/api/system-current.txt index 254d74aa235c..f6e76a246947 100644 --- a/location/api/system-current.txt +++ b/location/api/system-current.txt @@ -113,13 +113,13 @@ package android.location { } public final class GnssMeasurementRequest implements android.os.Parcelable { - method @FlaggedApi(Flags.FLAG_GNSS_API_MEASUREMENT_REQUEST_WORK_SOURCE) @NonNull public android.os.WorkSource getWorkSource(); + method @FlaggedApi("android.location.flags.gnss_api_measurement_request_work_source") @NonNull public android.os.WorkSource getWorkSource(); method public boolean isCorrelationVectorOutputsEnabled(); } public static final class GnssMeasurementRequest.Builder { method @NonNull public android.location.GnssMeasurementRequest.Builder setCorrelationVectorOutputsEnabled(boolean); - method @FlaggedApi(Flags.FLAG_GNSS_API_MEASUREMENT_REQUEST_WORK_SOURCE) @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public android.location.GnssMeasurementRequest.Builder setWorkSource(@Nullable android.os.WorkSource); + method @FlaggedApi("android.location.flags.gnss_api_measurement_request_work_source") @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public android.location.GnssMeasurementRequest.Builder setWorkSource(@Nullable android.os.WorkSource); } public final class GnssReflectingPlane implements android.os.Parcelable { @@ -591,7 +591,7 @@ package android.location { package android.location.provider { - @FlaggedApi(Flags.FLAG_NEW_GEOCODER) public final class ForwardGeocodeRequest implements android.os.Parcelable { + @FlaggedApi("android.location.flags.new_geocoder") public final class ForwardGeocodeRequest implements android.os.Parcelable { method public int describeContents(); method @Nullable public String getCallingAttributionTag(); method @NonNull public String getCallingPackage(); @@ -613,7 +613,7 @@ package android.location.provider { method @NonNull public android.location.provider.ForwardGeocodeRequest.Builder setCallingAttributionTag(@NonNull String); } - @FlaggedApi(Flags.FLAG_NEW_GEOCODER) public abstract class GeocodeProviderBase { + @FlaggedApi("android.location.flags.new_geocoder") public abstract class GeocodeProviderBase { ctor public GeocodeProviderBase(@NonNull android.content.Context, @NonNull String); method @NonNull public final android.os.IBinder getBinder(); method public abstract void onForwardGeocode(@NonNull android.location.provider.ForwardGeocodeRequest, @NonNull android.os.OutcomeReceiver<java.util.List<android.location.Address>,java.lang.Throwable>); @@ -672,7 +672,7 @@ package android.location.provider { method public void onProviderRequestChanged(@NonNull String, @NonNull android.location.provider.ProviderRequest); } - @FlaggedApi(Flags.FLAG_NEW_GEOCODER) public final class ReverseGeocodeRequest implements android.os.Parcelable { + @FlaggedApi("android.location.flags.new_geocoder") public final class ReverseGeocodeRequest implements android.os.Parcelable { method public int describeContents(); method @Nullable public String getCallingAttributionTag(); method @NonNull public String getCallingPackage(); diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_progressBar.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_progressBar.png Binary files differindex 105d1a108620..9d477c5b2226 100644 --- a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_progressBar.png +++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_progressBar.png diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_progressBar.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_progressBar.png Binary files differindex fc845a63efb4..f2e6d2ef3ed3 100644 --- a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_progressBar.png +++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_progressBar.png diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_progressBar.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_progressBar.png Binary files differindex 19c028e395b7..d31f2c4dcb5c 100644 --- a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_progressBar.png +++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_progressBar.png diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/ModifierExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/ModifierExt.kt new file mode 100644 index 000000000000..e883a4a55af9 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/ModifierExt.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.compose + +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics + +/** Sets the content description of this node. */ +fun Modifier.contentDescription(contentDescription: String?) = + if (contentDescription != null) this.semantics { + this.contentDescription = contentDescription + } else this diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt index f5cbe8ffcee3..d08d97eb89db 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt @@ -48,10 +48,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.android.settingslib.spa.debug.UiModePreviews +import com.android.settingslib.spa.framework.compose.contentDescription import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraLarge import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraSmall @@ -191,8 +190,7 @@ private fun Buttons(buttons: List<CardButton>, color: Color) { private fun Button(button: CardButton, color: Color) { TextButton( onClick = button.onClick, - modifier = - Modifier.semantics { button.contentDescription?.let { this.contentDescription = it } } + modifier = Modifier.contentDescription(button.contentDescription), ) { Text(text = button.text, color = color) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt index 22844362f949..bc5904cc8b9d 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsOutlinedTextField.kt @@ -40,6 +40,7 @@ fun SettingsOutlinedTextField( singleLine: Boolean = true, enabled: Boolean = true, shape: Shape = OutlinedTextFieldDefaults.shape, + placeholder: @Composable (() -> Unit)? = null, modifier: Modifier = Modifier .fillMaxWidth() .padding(SettingsDimension.textFieldPadding), @@ -60,6 +61,7 @@ fun SettingsOutlinedTextField( Text(text = errorMessage) } }, + placeholder = placeholder, shape = shape ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index 56d75d8bee4b..23a8e78e6c4a 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -41,7 +42,8 @@ internal fun BaseLayout( title: String, subTitle: @Composable () -> Unit, modifier: Modifier = Modifier, - icon: (@Composable () -> Unit)? = null, + titleContentDescription: String? = null, + icon: @Composable (() -> Unit)? = null, enabled: () -> Boolean = { true }, paddingStart: Dp = SettingsDimension.itemPaddingStart, paddingEnd: Dp = SettingsDimension.itemPaddingEnd, @@ -51,6 +53,7 @@ internal fun BaseLayout( Row( modifier = modifier .fillMaxWidth() + .semantics(mergeDescendants = true) {} .padding(end = paddingEnd), verticalAlignment = Alignment.CenterVertically, ) { @@ -58,6 +61,7 @@ internal fun BaseLayout( BaseIcon(icon, alphaModifier, paddingStart) Titles( title = title, + titleContentDescription = titleContentDescription, subTitle = subTitle, modifier = alphaModifier .weight(1f) @@ -87,9 +91,14 @@ internal fun BaseIcon( // Extracts a scope to avoid frequent recompose outside scope. @Composable -private fun Titles(title: String, subTitle: @Composable () -> Unit, modifier: Modifier) { +private fun Titles( + title: String, + titleContentDescription: String?, + subTitle: @Composable () -> Unit, + modifier: Modifier, +) { Column(modifier) { - SettingsTitle(title) + SettingsTitle(title, titleContentDescription) subTitle() } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt index 194ed81df0ee..e9b3ba2e01f1 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt @@ -32,6 +32,8 @@ internal fun BasePreference( title: String, summary: () -> String, modifier: Modifier = Modifier, + titleContentDescription: String? = null, + summaryContentDescription: () -> String? = { null }, singleLineSummary: Boolean = false, icon: @Composable (() -> Unit)? = null, enabled: () -> Boolean = { true }, @@ -42,9 +44,11 @@ internal fun BasePreference( ) { BaseLayout( title = title, + titleContentDescription = titleContentDescription, subTitle = { SettingsBody( body = summary(), + contentDescription = summaryContentDescription(), maxLines = if (singleLineSummary) 1 else Int.MAX_VALUE, ) }, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt index 3acf075d8900..4ad4c1496ce8 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt @@ -64,12 +64,24 @@ interface PreferenceModel { val title: String /** + * The content description of [title]. + */ + val titleContentDescription: String? + get() = null + + /** * The summary of this [Preference]. */ val summary: () -> String get() = { "" } /** + * The content description of [summary]. + */ + val summaryContentDescription: () -> String? + get() = { null } + + /** * The icon of this [Preference]. * * Default is `null` which means no icon. @@ -112,7 +124,9 @@ fun Preference( EntryHighlight { BasePreference( title = model.title, + titleContentDescription = model.titleContentDescription, summary = model.summary, + summaryContentDescription = model.summaryContentDescription, singleLineSummary = singleLineSummary, modifier = modifier, icon = model.icon, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt index 5155406b6d79..2fac576952be 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt @@ -21,8 +21,7 @@ import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics +import com.android.settingslib.spa.framework.compose.contentDescription import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog @Composable @@ -37,9 +36,7 @@ internal fun SettingsSwitch( Switch( checked = checked, onCheckedChange = wrapOnSwitchWithLog(onCheckedChange), - modifier = if (contentDescription != null) Modifier.semantics { - this.contentDescription = contentDescription - } else Modifier, + modifier = Modifier.contentDescription(contentDescription), enabled = changeable(), interactionSource = interactionSource, ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt index a59b95a60879..d423d9fe5897 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt @@ -30,16 +30,23 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.compose.contentDescription import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.theme.toMediumWeight @Composable -fun SettingsTitle(title: String, useMediumWeight: Boolean = false) { +fun SettingsTitle( + title: String, + contentDescription: String? = null, + useMediumWeight: Boolean = false, +) { Text( text = title, - modifier = Modifier.padding(vertical = SettingsDimension.paddingTiny), + modifier = Modifier + .padding(vertical = SettingsDimension.paddingTiny) + .contentDescription(contentDescription), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleMedium.withWeight(useMediumWeight), ) @@ -81,11 +88,13 @@ fun SettingsListItem(text: String, enabled: Boolean = true) { @Composable fun SettingsBody( body: String, + contentDescription: String? = null, maxLines: Int = Int.MAX_VALUE, ) { if (body.isNotEmpty()) { Text( text = body, + modifier = Modifier.contentDescription(contentDescription), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium, overflow = TextOverflow.Ellipsis, diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt index 8c363db92e19..5ef3329541f2 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.test.assertHeightIsAtLeast import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -93,11 +94,10 @@ class PreferenceTest { } } - val summaryNode = composeTestRule.onNodeWithText(LONG_SUMMARY) try { // There is no assertHeightIsAtMost, so use the assertHeightIsAtLeast and catch the // expected exception. - summaryNode.assertHeightIsAtLeast(lineHeightDp.times(2)) + composeTestRule.onRoot().assertHeightIsAtLeast(lineHeightDp.times(5)) } catch (e: AssertionError) { assertThat(e).hasMessageThat().contains("height") return diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java index 5ed59996bee3..2da622139b79 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java @@ -16,6 +16,8 @@ package com.android.settingslib.utils; +import static java.lang.Math.abs; + import android.content.Context; import android.icu.text.DateFormat; import android.icu.text.MeasureFormat; @@ -212,8 +214,8 @@ public class PowerUtil { * @return The rounded value as a long */ public static long roundTimeToNearestThreshold(long drainTime, long threshold) { - long time = Math.abs(drainTime); - long multiple = Math.abs(threshold); + long time = abs(drainTime); + long multiple = abs(threshold); final long remainder = time % multiple; if (remainder < multiple / 2) { return time - remainder; @@ -222,18 +224,24 @@ public class PowerUtil { } } - /** Gets the rounded target time string in a short format. */ + /** Gets the target time string in a short format. */ public static String getTargetTimeShortString( Context context, long targetTimeOffsetMs, long currentTimeMs) { - final long roundedTimeOfDayMs = - roundTimeToNearestThreshold( - currentTimeMs + targetTimeOffsetMs, FIFTEEN_MINUTES_MILLIS); + long targetTimeMs = currentTimeMs + targetTimeOffsetMs; + if (targetTimeOffsetMs >= FIFTEEN_MINUTES_MILLIS) { + targetTimeMs = roundUpTimeToNextThreshold(targetTimeMs, FIFTEEN_MINUTES_MILLIS); + } // convert the time to a properly formatted string. String skeleton = android.text.format.DateFormat.getTimeFormatString(context); DateFormat fmt = DateFormat.getInstanceForSkeleton(skeleton); - Date date = Date.from(Instant.ofEpochMilli(roundedTimeOfDayMs)); + Date date = Date.from(Instant.ofEpochMilli(targetTimeMs)); return fmt.format(date); } -} + private static long roundUpTimeToNextThreshold(long timeMs, long threshold) { + var time = abs(timeMs); + var multiple = abs(threshold); + return ((time + multiple - 1) / multiple) * multiple; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java index cbc382b6b920..4f3b2005b197 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java @@ -87,15 +87,31 @@ public class PowerUtilTest { } @Test - public void getTargetTimeShortString_returnsTimeShortString() { + public void getTargetTimeShortString_lessThan15Minutes_returnsTimeShortStringWithoutRounded() { mContext.getSystemService(AlarmManager.class).setTimeZone("UTC"); mContext.getResources().getConfiguration().setLocale(Locale.US); var currentTimeMs = Instant.parse("2024-06-06T15:00:00Z").toEpochMilli(); - var remainingTimeMs = Duration.ofMinutes(30).toMillis(); + var remainingTimeMs = Duration.ofMinutes(15).toMillis() - 1; var actualTimeString = PowerUtil.getTargetTimeShortString(mContext, remainingTimeMs, currentTimeMs); - assertThat(actualTimeString).isEqualTo("3:30 PM"); + // due to timezone issue in test case, focus on rounded minutes, remove hours part. + assertThat(actualTimeString).endsWith("14 PM"); + } + + @Test + public void getTargetTimeShortString_moreThan15Minutes_returnsTimeShortStringWithRounded() { + mContext.getSystemService(AlarmManager.class).setTimeZone("UTC"); + mContext.getResources().getConfiguration().setLocale(Locale.US); + var currentTimeMs = Instant.parse("2024-06-06T15:00:00Z").toEpochMilli(); + var remainingTimeMs = Duration.ofMinutes(15).toMillis() + 1; + + var actualTimeString = + PowerUtil.getTargetTimeShortString(mContext, remainingTimeMs, currentTimeMs); + + // due to timezone issue in test case, focus on rounded minutes, remove hours part. + assertThat(actualTimeString).endsWith("30 PM"); + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java index df27cbb070b6..027f6744d4d7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java @@ -35,6 +35,8 @@ public class BiometricNotificationBroadcastReceiver extends BroadcastReceiver { static final String ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG = "fingerprint_action_show_reenroll_dialog"; + static final String EXTRA_IS_REENROLL_FORCED = "is_reenroll_forced"; + private static final String TAG = "BiometricNotificationBroadcastReceiver"; private final Context mContext; @@ -56,14 +58,16 @@ public class BiometricNotificationBroadcastReceiver extends BroadcastReceiver { mNotificationDialogFactory.createReenrollDialog( mContext.getUserId(), mContext::startActivity, - BiometricSourceType.FACE) + BiometricSourceType.FACE, + false) .show(); break; case ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG: mNotificationDialogFactory.createReenrollDialog( mContext.getUserId(), mContext::startActivity, - BiometricSourceType.FINGERPRINT) + BiometricSourceType.FINGERPRINT, + intent.getBooleanExtra(EXTRA_IS_REENROLL_FORCED, false)) .show(); break; default: diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java index fd0feef7689b..4ac5a12dae03 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java @@ -29,13 +29,12 @@ import android.hardware.fingerprint.FingerprintManager; import android.provider.Settings; import android.util.Log; -import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.SystemUIDialog; import javax.inject.Inject; -import javax.inject.Provider; /** * Manages the creation of dialogs to be shown for biometric re enroll notifications. @@ -61,7 +60,8 @@ public class BiometricNotificationDialogFactory { } Dialog createReenrollDialog( - int userId, ActivityStarter activityStarter, BiometricSourceType biometricSourceType) { + int userId, ActivityStarter activityStarter, BiometricSourceType biometricSourceType, + boolean isReenrollForced) { SystemUIDialog sysuiDialog = mSystemUIDialogFactory.create(); if (biometricSourceType == BiometricSourceType.FACE) { sysuiDialog.setTitle(mResources.getString(R.string.face_re_enroll_dialog_title)); @@ -80,8 +80,12 @@ public class BiometricNotificationDialogFactory { sysuiDialog.setPositiveButton(R.string.biometric_re_enroll_dialog_confirm, (dialog, which) -> onReenrollDialogConfirm( userId, biometricSourceType, activityStarter)); - sysuiDialog.setNegativeButton(R.string.biometric_re_enroll_dialog_cancel, - (dialog, which) -> {}); + if (!isReenrollForced) { + sysuiDialog.setNegativeButton(R.string.biometric_re_enroll_dialog_cancel, + (dialog, which) -> { + }); + } + sysuiDialog.setCanceledOnTouchOutside(!isReenrollForced); return sysuiDialog; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java index d6a4cbb67487..3b49ce2f10f7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java @@ -17,6 +17,7 @@ package com.android.systemui.biometrics; import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FACE_REENROLL_DIALOG; import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG; @@ -43,8 +44,8 @@ import android.util.Log; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.CoreStartable; -import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.KeyguardStateController; import java.util.Optional; @@ -80,6 +81,8 @@ public class BiometricNotificationService implements CoreStartable { private boolean mFingerprintNotificationQueued; private boolean mFingerprintReenrollRequired; + private boolean mIsFingerprintReenrollForced; + private final KeyguardStateController.Callback mKeyguardStateControllerCallback = new KeyguardStateController.Callback() { private boolean mIsShowing = true; @@ -118,9 +121,11 @@ public class BiometricNotificationService implements CoreStartable { public void onBiometricHelp(int msgId, String helpString, BiometricSourceType biometricSourceType) { if (biometricSourceType == BiometricSourceType.FINGERPRINT - && mFingerprintReEnrollNotification.isFingerprintReEnrollRequired( + && mFingerprintReEnrollNotification.isFingerprintReEnrollRequested( msgId)) { mFingerprintReenrollRequired = true; + mIsFingerprintReenrollForced = + mFingerprintReEnrollNotification.isFingerprintReEnrollForced(msgId); } } }; @@ -191,7 +196,7 @@ public class BiometricNotificationService implements CoreStartable { final String name = mContext.getString(R.string.face_re_enroll_notification_name); mHandler.postDelayed( () -> showNotification(ACTION_SHOW_FACE_REENROLL_DIALOG, title, content, name, - FACE_NOTIFICATION_ID), + FACE_NOTIFICATION_ID, false), SHOW_NOTIFICATION_DELAY_MS); } @@ -204,12 +209,12 @@ public class BiometricNotificationService implements CoreStartable { final String name = mContext.getString(R.string.fingerprint_re_enroll_notification_name); mHandler.postDelayed( () -> showNotification(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG, title, content, - name, FINGERPRINT_NOTIFICATION_ID), + name, FINGERPRINT_NOTIFICATION_ID, mIsFingerprintReenrollForced), SHOW_NOTIFICATION_DELAY_MS); } private void showNotification(String action, CharSequence title, CharSequence content, - CharSequence name, int notificationId) { + CharSequence name, int notificationId, boolean isReenrollForced) { if (notificationId == FACE_NOTIFICATION_ID) { mFaceNotificationQueued = false; } else if (notificationId == FINGERPRINT_NOTIFICATION_ID) { @@ -223,8 +228,12 @@ public class BiometricNotificationService implements CoreStartable { } final Intent onClickIntent = new Intent(action); + onClickIntent.putExtra(BiometricNotificationBroadcastReceiver.EXTRA_IS_REENROLL_FORCED, + isReenrollForced); + final PendingIntent onClickPendingIntent = PendingIntent.getBroadcastAsUser(mContext, - 0 /* requestCode */, onClickIntent, FLAG_IMMUTABLE, UserHandle.CURRENT); + 0 /* requestCode */, onClickIntent, FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT, + UserHandle.CURRENT); final Notification notification = new Notification.Builder(mContext, CHANNEL_ID) .setCategory(Notification.CATEGORY_SYSTEM) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java index 9050f26d39e4..5b9ed483da02 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotification.java @@ -23,7 +23,16 @@ import android.hardware.biometrics.BiometricFingerprintConstants; */ public interface FingerprintReEnrollNotification { //TODO: Remove this class and add a constant in the HAL API instead (b/281841852) - /** Returns true if msgId corresponds to FINGERPRINT_ACQUIRED_RE_ENROLL. */ - boolean isFingerprintReEnrollRequired( + /** + * Returns true if msgId corresponds to FINGERPRINT_ACQUIRED_RE_ENROLL_OPTIONAL or + * FINGERPRINT_ACQUIRED_RE_ENROLL_FORCED. + */ + boolean isFingerprintReEnrollRequested( + @BiometricFingerprintConstants.FingerprintAcquired int msgId); + + /** + * Returns true if msgId corresponds to FINGERPRINT_ACQUIRED_RE_ENROLL_FORCED. + */ + boolean isFingerprintReEnrollForced( @BiometricFingerprintConstants.FingerprintAcquired int msgId); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java index 1f86bc6ae298..d47e1e67796e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintReEnrollNotificationImpl.java @@ -23,7 +23,13 @@ import android.hardware.biometrics.BiometricFingerprintConstants; */ public class FingerprintReEnrollNotificationImpl implements FingerprintReEnrollNotification{ @Override - public boolean isFingerprintReEnrollRequired(int msgId) { - return msgId == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_RE_ENROLL; + public boolean isFingerprintReEnrollRequested(int msgId) { + return msgId == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_RE_ENROLL_OPTIONAL + || msgId == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_RE_ENROLL_FORCED; + } + + @Override + public boolean isFingerprintReEnrollForced(int msgId) { + return msgId == BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_RE_ENROLL_FORCED; } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 4ee2db796aef..042fb63f949c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -75,6 +75,10 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { @Override protected void onFinishInflate() { super.onFinishInflate(); + // set layer to make alpha animation of brightness slider nicer - otherwise elements + // of slider are animated separately and it doesn't look good. See b/329244723 + setLayerType(LAYER_TYPE_HARDWARE, null); + mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view); mQSPanel = findViewById(R.id.quick_settings_panel); mHeader = findViewById(R.id.header); diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java index 92006a473ed8..e051dab3a2b5 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderView.java @@ -63,7 +63,6 @@ public class BrightnessSliderView extends FrameLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - setLayerType(LAYER_TYPE_HARDWARE, null); mSlider = requireViewById(R.id.slider); mSlider.setAccessibilityLabel(getContentDescription().toString()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index 4ebb6998b8c7..271b0a86ca12 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -39,13 +39,13 @@ import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext /** * Inflates and updates icons associated with notifications @@ -239,8 +239,8 @@ constructor( val sbi = icon.toStatusBarIcon(entry) - // Cache if important conversation. - if (isImportantConversation(entry)) { + // Cache if important conversation or app icon. + if (isImportantConversation(entry) || android.app.Flags.notificationsUseAppIcon()) { if (showPeopleAvatar) { entry.icons.peopleAvatarDescriptor = sbi } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt index bfeaced72162..2fdd2c6434cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt @@ -20,6 +20,7 @@ import android.graphics.Rect import android.view.View import com.android.app.tracing.traceSection import com.android.internal.util.ContrastColorUtil +import com.android.systemui.Flags import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.NO_COLOR @@ -34,9 +35,12 @@ object StatusBarIconViewBinder { // view-model (which, at the time of this writing, does not yet exist). suspend fun bindColor(view: StatusBarIconView, color: Flow<Int>) { - color.collectTracingEach("SBIV#bindColor") { color -> - view.staticDrawableColor = color - view.setDecorColor(color) + // Don't change the icon color if an app icon experiment is enabled. + if (!android.app.Flags.notificationsUseAppIcon()) { + color.collectTracingEach("SBIV#bindColor") { color -> + view.staticDrawableColor = color + view.setDecorColor(color) + } } } @@ -53,12 +57,15 @@ object StatusBarIconViewBinder { iconColors: Flow<NotificationIconColors>, contrastColorUtil: ContrastColorUtil, ) { - iconColors.collectTracingEach("SBIV#bindIconColors") { colors -> - val isPreL = java.lang.Boolean.TRUE == view.getTag(R.id.icon_is_pre_L) - val isColorized = !isPreL || NotificationUtils.isGrayscale(view, contrastColorUtil) - view.staticDrawableColor = - if (isColorized) colors.staticDrawableColor(view.viewBounds) else NO_COLOR - view.setDecorColor(colors.tint) + // Don't change the icon color if an app icon experiment is enabled. + if (!android.app.Flags.notificationsUseAppIcon()) { + iconColors.collectTracingEach("SBIV#bindIconColors") { colors -> + val isPreL = java.lang.Boolean.TRUE == view.getTag(R.id.icon_is_pre_L) + val isColorized = !isPreL || NotificationUtils.isGrayscale(view, contrastColorUtil) + view.staticDrawableColor = + if (isColorized) colors.staticDrawableColor(view.viewBounds) else NO_COLOR + view.setDecorColor(colors.tint) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt index 6acb12a3e2af..52cb48be041f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt @@ -16,13 +16,12 @@ package com.android.systemui.statusbar.notification.ui.viewbinder -import android.util.Log -import com.android.systemui.common.coroutine.ConflatedCallbackFlow import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel import com.android.systemui.util.kotlin.sample +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope @@ -77,8 +76,7 @@ constructor(private val viewModel: NotificationListViewModel) { } private val NotificationStackScrollLayout.isHeadsUpAnimatingAway: Flow<Boolean> - get() = - ConflatedCallbackFlow.conflatedCallbackFlow { - setHeadsUpAnimatingAwayListener { animatingAway -> trySend(animatingAway) } - awaitClose { setHeadsUpAnimatingAwayListener(null) } - } + get() = conflatedCallbackFlow { + setHeadsUpAnimatingAwayListener { animatingAway -> trySend(animatingAway) } + awaitClose { setHeadsUpAnimatingAwayListener(null) } + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java index 8c8544cd6e5b..d2c695739ea9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java @@ -17,6 +17,7 @@ package com.android.systemui.biometrics; import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -90,7 +91,8 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { assumeTrue(getContext().getPackageManager() .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)); - mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FINGERPRINT); + mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FINGERPRINT, + false); verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); @@ -115,7 +117,8 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { assumeTrue(getContext().getPackageManager() .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)); - mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FINGERPRINT); + mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FINGERPRINT, + false); verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); @@ -134,11 +137,25 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { } @Test + public void testFingerprintReEnrollDialog_forced() { + assumeTrue(getContext().getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)); + + mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FINGERPRINT, + true); + + verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); + + verify(mDialog, never()).setNegativeButton(anyInt(), any()); + } + + @Test public void testFaceReEnrollDialog_onRemovalSucceeded() { assumeTrue(getContext().getPackageManager() .hasSystemFeature(PackageManager.FEATURE_FACE)); - mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FACE); + mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FACE, + false); verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); @@ -163,7 +180,8 @@ public class BiometricNotificationDialogFactoryTest extends SysuiTestCase { assumeTrue(getContext().getPackageManager() .hasSystemFeature(PackageManager.FEATURE_FACE)); - mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FACE); + mDialogFactory.createReenrollDialog(0, mActivityStarter, BiometricSourceType.FACE, + false); verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java index c6771b262114..a279d3ee67e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java @@ -18,7 +18,9 @@ package com.android.systemui.biometrics; import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FACE_REENROLL_DIALOG; import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; @@ -98,7 +100,7 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { public void setUp() { when(mFingerprintReEnrollNotificationOptional.orElse(any())) .thenReturn(mFingerprintReEnrollNotification); - when(mFingerprintReEnrollNotification.isFingerprintReEnrollRequired( + when(mFingerprintReEnrollNotification.isFingerprintReEnrollRequested( FINGERPRINT_ACQUIRED_RE_ENROLL)).thenReturn(true); mLooper = TestableLooper.get(this); @@ -140,8 +142,37 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { } @Test - public void testShowFingerprintReEnrollNotification_onAcquiredReEnroll() { + public void testShowFingerprintReEnrollNotification_onAcquiredReEnroll_Optional() { when(mKeyguardStateController.isShowing()).thenReturn(false); + when(mFingerprintReEnrollNotification.isFingerprintReEnrollForced( + FINGERPRINT_ACQUIRED_RE_ENROLL)).thenReturn(false); + + mKeyguardUpdateMonitorCallback.onBiometricHelp( + FINGERPRINT_ACQUIRED_RE_ENROLL, + "Testing Fingerprint Re-enrollment" /* errString */, + BiometricSourceType.FINGERPRINT + ); + mKeyguardStateControllerCallback.onKeyguardShowingChanged(); + + mLooper.moveTimeForward(SHOW_NOTIFICATION_DELAY_MS); + mLooper.processAllMessages(); + + verify(mNotificationManager).notifyAsUser(eq(TAG), eq(FINGERPRINT_NOTIFICATION_ID), + mNotificationArgumentCaptor.capture(), any()); + + Notification fingerprintNotification = mNotificationArgumentCaptor.getValue(); + + assertThat(fingerprintNotification.contentIntent.getIntent().getAction()) + .isEqualTo(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG); + assertThat(fingerprintNotification.contentIntent.getIntent().getBooleanExtra( + BiometricNotificationBroadcastReceiver.EXTRA_IS_REENROLL_FORCED, false)).isFalse(); + } + + @Test + public void testShowFingerprintReEnrollNotification_onAcquiredReEnroll_force() { + when(mKeyguardStateController.isShowing()).thenReturn(false); + when(mFingerprintReEnrollNotification.isFingerprintReEnrollForced( + FINGERPRINT_ACQUIRED_RE_ENROLL)).thenReturn(true); mKeyguardUpdateMonitorCallback.onBiometricHelp( FINGERPRINT_ACQUIRED_RE_ENROLL, @@ -160,6 +191,8 @@ public class BiometricNotificationServiceTest extends SysuiTestCase { assertThat(fingerprintNotification.contentIntent.getIntent().getAction()) .isEqualTo(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG); + assertThat(fingerprintNotification.contentIntent.getIntent().getBooleanExtra( + BiometricNotificationBroadcastReceiver.EXTRA_IS_REENROLL_FORCED, false)).isTrue(); } @Test public void testShowFaceReEnrollNotification_onErrorReEnroll() { diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 1bd93e4dbc36..b8bfedae91ae 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -4727,9 +4727,14 @@ public class AppOpsService extends IAppOpsService.Stub { } if ((code == OP_CAMERA) && isAutomotive()) { - if ((Flags.cameraPrivacyAllowlist()) - && (mSensorPrivacyManager.isCameraPrivacyEnabled(packageName))) { - return true; + final long identity = Binder.clearCallingIdentity(); + try { + if ((Flags.cameraPrivacyAllowlist()) + && (mSensorPrivacyManager.isCameraPrivacyEnabled(packageName))) { + return true; + } + } finally { + Binder.restoreCallingIdentity(identity); } } diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java index 32a0ef4c6a42..8002300dacc0 100644 --- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java +++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java @@ -18,6 +18,7 @@ package com.android.server.location.gnss; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import android.annotation.RequiresPermission; import android.content.Context; import android.location.flags.Flags; import android.net.ConnectivityManager; @@ -32,6 +33,7 @@ import android.os.Looper; import android.os.PowerManager; import android.telephony.PhoneStateListener; import android.telephony.PreciseCallState; +import android.telephony.ServiceState; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -572,6 +574,10 @@ class GnssNetworkConnectivityHandler { } } + @RequiresPermission(allOf = { + android.Manifest.permission.ACCESS_COARSE_LOCATION, + android.Manifest.permission.READ_PHONE_STATE + }) private void handleRequestSuplConnection(int agpsType, byte[] suplIpAddr) { mAGpsDataConnectionIpAddr = null; mAGpsType = agpsType; @@ -605,6 +611,19 @@ class GnssNetworkConnectivityHandler { NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder(); networkRequestBuilder.addCapability(getNetworkCapability(mAGpsType)); networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + + if (com.android.internal.telephony.flags.Flags.satelliteInternet()) { + // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network. + TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); + if (telephonyManager != null) { + ServiceState state = telephonyManager.getServiceState(); + if (state != null && state.isUsingNonTerrestrialNetwork()) { + networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_SATELLITE); + } + } + } + // During an emergency call, and when we have cached the Active Sub Id, we set the // Network Specifier so that the network request goes to the correct Sub Id if (mNiHandler.getInEmergency() && mActiveSubId >= 0) { diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 3d6855547bcd..a3c5d2d336f2 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -66,7 +66,6 @@ import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; -import android.os.DeadObjectException; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -90,7 +89,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; @@ -191,6 +189,9 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions; private final Object mLock = new Object(); + // This field is partially guarded by mLock. Writes and non-atomic iterations (for example: + // index-based-iterations) must be guarded by mLock. But it is safe to acquire an iterator + // without acquiring mLock. private final CopyOnWriteArrayList<ISessionControllerCallbackHolder> mControllerCallbackHolders = new CopyOnWriteArrayList<>(); @@ -886,24 +887,9 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } playbackState = mPlaybackState; } - Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null; - for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { - try { - holder.mCallback.onPlaybackStateChanged(playbackState); - } catch (DeadObjectException e) { - if (deadCallbackHolders == null) { - deadCallbackHolders = new ArrayList<>(); - } - deadCallbackHolders.add(holder); - logCallbackException("Removing dead callback in pushPlaybackStateUpdate", holder, - e); - } catch (RemoteException e) { - logCallbackException("unexpected exception in pushPlaybackStateUpdate", holder, e); - } - } - if (deadCallbackHolders != null) { - removeControllerHoldersSafely(deadCallbackHolders); - } + performOnCallbackHolders( + "pushPlaybackStateUpdate", + holder -> holder.mCallback.onPlaybackStateChanged(playbackState)); } private void pushMetadataUpdate() { @@ -914,23 +900,8 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } metadata = mMetadata; } - Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null; - for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { - try { - holder.mCallback.onMetadataChanged(metadata); - } catch (DeadObjectException e) { - if (deadCallbackHolders == null) { - deadCallbackHolders = new ArrayList<>(); - } - deadCallbackHolders.add(holder); - logCallbackException("Removing dead callback in pushMetadataUpdate", holder, e); - } catch (RemoteException e) { - logCallbackException("unexpected exception in pushMetadataUpdate", holder, e); - } - } - if (deadCallbackHolders != null) { - removeControllerHoldersSafely(deadCallbackHolders); - } + performOnCallbackHolders( + "pushMetadataUpdate", holder -> holder.mCallback.onMetadataChanged(metadata)); } private void pushQueueUpdate() { @@ -941,31 +912,18 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } toSend = mQueue == null ? null : new ArrayList<>(mQueue); } - Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null; - for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { - ParceledListSlice<QueueItem> parcelableQueue = null; - if (toSend != null) { - parcelableQueue = new ParceledListSlice<>(toSend); - // Limit the size of initial Parcel to prevent binder buffer overflow - // as onQueueChanged is an async binder call. - parcelableQueue.setInlineCountLimit(1); - } - - try { - holder.mCallback.onQueueChanged(parcelableQueue); - } catch (DeadObjectException e) { - if (deadCallbackHolders == null) { - deadCallbackHolders = new ArrayList<>(); - } - deadCallbackHolders.add(holder); - logCallbackException("Removing dead callback in pushQueueUpdate", holder, e); - } catch (RemoteException e) { - logCallbackException("unexpected exception in pushQueueUpdate", holder, e); - } - } - if (deadCallbackHolders != null) { - removeControllerHoldersSafely(deadCallbackHolders); - } + performOnCallbackHolders( + "pushQueueUpdate", + holder -> { + ParceledListSlice<QueueItem> parcelableQueue = null; + if (toSend != null) { + parcelableQueue = new ParceledListSlice<>(toSend); + // Limit the size of initial Parcel to prevent binder buffer overflow + // as onQueueChanged is an async binder call. + parcelableQueue.setInlineCountLimit(1); + } + holder.mCallback.onQueueChanged(parcelableQueue); + }); } private void pushQueueTitleUpdate() { @@ -976,23 +934,8 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } queueTitle = mQueueTitle; } - Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null; - for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { - try { - holder.mCallback.onQueueTitleChanged(queueTitle); - } catch (DeadObjectException e) { - if (deadCallbackHolders == null) { - deadCallbackHolders = new ArrayList<>(); - } - deadCallbackHolders.add(holder); - logCallbackException("Removing dead callback in pushQueueTitleUpdate", holder, e); - } catch (RemoteException e) { - logCallbackException("unexpected exception in pushQueueTitleUpdate", holder, e); - } - } - if (deadCallbackHolders != null) { - removeControllerHoldersSafely(deadCallbackHolders); - } + performOnCallbackHolders( + "pushQueueTitleUpdate", holder -> holder.mCallback.onQueueTitleChanged(queueTitle)); } private void pushExtrasUpdate() { @@ -1003,23 +946,8 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } extras = mExtras; } - Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null; - for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { - try { - holder.mCallback.onExtrasChanged(extras); - } catch (DeadObjectException e) { - if (deadCallbackHolders == null) { - deadCallbackHolders = new ArrayList<>(); - } - deadCallbackHolders.add(holder); - logCallbackException("Removing dead callback in pushExtrasUpdate", holder, e); - } catch (RemoteException e) { - logCallbackException("unexpected exception in pushExtrasUpdate", holder, e); - } - } - if (deadCallbackHolders != null) { - removeControllerHoldersSafely(deadCallbackHolders); - } + performOnCallbackHolders( + "pushExtrasUpdate", holder -> holder.mCallback.onExtrasChanged(extras)); } private void pushVolumeUpdate() { @@ -1030,23 +958,8 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } info = getVolumeAttributes(); } - Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null; - for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { - try { - holder.mCallback.onVolumeInfoChanged(info); - } catch (DeadObjectException e) { - if (deadCallbackHolders == null) { - deadCallbackHolders = new ArrayList<>(); - } - deadCallbackHolders.add(holder); - logCallbackException("Removing dead callback in pushVolumeUpdate", holder, e); - } catch (RemoteException e) { - logCallbackException("unexpected exception in pushVolumeUpdate", holder, e); - } - } - if (deadCallbackHolders != null) { - removeControllerHoldersSafely(deadCallbackHolders); - } + performOnCallbackHolders( + "pushVolumeUpdate", holder -> holder.mCallback.onVolumeInfoChanged(info)); } private void pushEvent(String event, Bundle data) { @@ -1055,23 +968,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde return; } } - Collection<ISessionControllerCallbackHolder> deadCallbackHolders = null; - for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { - try { - holder.mCallback.onEvent(event, data); - } catch (DeadObjectException e) { - if (deadCallbackHolders == null) { - deadCallbackHolders = new ArrayList<>(); - } - deadCallbackHolders.add(holder); - logCallbackException("Removing dead callback in pushEvent", holder, e); - } catch (RemoteException e) { - logCallbackException("unexpected exception in pushEvent", holder, e); - } - } - if (deadCallbackHolders != null) { - removeControllerHoldersSafely(deadCallbackHolders); - } + performOnCallbackHolders("pushEvent", holder -> holder.mCallback.onEvent(event, data)); } private void pushSessionDestroyed() { @@ -1082,20 +979,37 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde return; } } + performOnCallbackHolders( + "pushSessionDestroyed", + holder -> { + holder.mCallback.asBinder().unlinkToDeath(holder.mDeathMonitor, 0); + holder.mCallback.onSessionDestroyed(); + }); + // After notifying clear all listeners + synchronized (mLock) { + mControllerCallbackHolders.clear(); + } + } + + private interface ControllerCallbackCall { + + void performOn(ISessionControllerCallbackHolder holder) throws RemoteException; + } + + private void performOnCallbackHolders(String operationName, ControllerCallbackCall call) { + ArrayList<ISessionControllerCallbackHolder> deadCallbackHolders = new ArrayList<>(); for (ISessionControllerCallbackHolder holder : mControllerCallbackHolders) { try { - holder.mCallback.asBinder().unlinkToDeath(holder.mDeathMonitor, 0); - holder.mCallback.onSessionDestroyed(); - } catch (NoSuchElementException e) { - logCallbackException("error unlinking to binder death", holder, e); - } catch (DeadObjectException e) { - logCallbackException("Removing dead callback in pushSessionDestroyed", holder, e); - } catch (RemoteException e) { - logCallbackException("unexpected exception in pushSessionDestroyed", holder, e); + call.performOn(holder); + } catch (RemoteException | NoSuchElementException exception) { + deadCallbackHolders.add(holder); + logCallbackException( + "Exception while executing: " + operationName, holder, exception); } } - // After notifying clear all listeners - removeControllerHoldersSafely(null); + synchronized (mLock) { + mControllerCallbackHolders.removeAll(deadCallbackHolders); + } } private PlaybackState getStateWithUpdatedPosition() { @@ -1143,17 +1057,6 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde return -1; } - private void removeControllerHoldersSafely( - Collection<ISessionControllerCallbackHolder> holders) { - synchronized (mLock) { - if (holders == null) { - mControllerCallbackHolders.clear(); - } else { - mControllerCallbackHolders.removeAll(holders); - } - } - } - private PlaybackInfo getVolumeAttributes() { int volumeType; AudioAttributes attributes; diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 20b7fd46beb5..4a3812bf0f49 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1493,9 +1493,13 @@ public class ZenModeHelper { newConfig = mConfig.copy(); if (zenMode == Global.ZEN_MODE_OFF) { newConfig.manualRule = null; - for (ZenRule automaticRule : newConfig.automaticRules.values()) { - if (automaticRule.isAutomaticActive()) { - automaticRule.snoozing = true; + if (!Flags.modesUi() || origin != UPDATE_ORIGIN_USER) { + // User deactivation of DND means just turning off the manual DND rule. + // For API calls (different origin) keep old behavior of snoozing all rules. + for (ZenRule automaticRule : newConfig.automaticRules.values()) { + if (automaticRule.isAutomaticActive()) { + automaticRule.snoozing = true; + } } } } else { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 91860a6db942..390a7cf2d09e 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -4805,6 +4805,14 @@ class Task extends TaskFragment { topActivity.getSyncTransaction()); } lastParentBeforePip.moveToFront("movePinnedActivityToOriginalTask"); + // If the reparent is not included in transition, make sure the visibility of + // task is still updated by core. Otherwise if the task is collected (e.g. + // rotation change) after leaving this scope, the visibility operation will be + // put in sync transaction, then it is not synced with reparent. + if (com.android.window.flags.Flags.removePrepareSurfaceInPlacement() + && lastParentBeforePip.mSyncState == SYNC_STATE_NONE) { + lastParentBeforePip.prepareSurfaces(); + } } if (isPip2ExperimentEnabled) { super.setWindowingMode(windowingMode); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 7e083ba8859a..be235b3bef92 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -821,6 +821,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public static final long THROW_SECURITY_EXCEPTION_FOR_SENSOR_PERMISSIONS = 277035314L; + /** + * Allows DPCs to provisioning fully managed headless devices in single-user mode. + */ + @ChangeId + @EnabledSince(targetSdkVersion = 35) + public static final long PROVISION_SINGLE_USER_MODE = 289515470L; + // Only add to the end of the list. Do not change or rearrange these values, that will break // historical data. Do not use negative numbers or zero, logger only handles positive // integers. @@ -6834,7 +6841,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // If there is a profile owner, redirect to that; otherwise query the device owner. ComponentName aliasChooser = getProfileOwnerAsUser(caller.getUserId()); - if (aliasChooser == null && caller.getUserHandle().isSystem()) { + boolean isDoUser = Flags.headlessSingleUserFixes() + ? caller.getUserId() == getDeviceOwnerUserId() + : caller.getUserHandle().isSystem(); + if (aliasChooser == null && isDoUser) { synchronized (getLockObject()) { final ActiveAdmin deviceOwnerAdmin = getDeviceOwnerAdminLocked(); if (deviceOwnerAdmin != null) { @@ -7828,7 +7838,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mInjector.binderWithCleanCallingIdentity(() -> { // First check whether the admin is allowed to wipe the device/user/profile. final String restriction; - if (userId == UserHandle.USER_SYSTEM) { + boolean shouldFactoryReset = userId == UserHandle.USER_SYSTEM; + if (Flags.headlessSingleUserFixes() && getHeadlessDeviceOwnerModeForDeviceOwner() + == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER) { + shouldFactoryReset = userId == getMainUserId(); + } + if (shouldFactoryReset) { restriction = UserManager.DISALLOW_FACTORY_RESET; } else if (isManagedProfile(userId)) { restriction = UserManager.DISALLOW_REMOVE_MANAGED_PROFILE; @@ -7842,12 +7857,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { }); boolean isSystemUser = userId == UserHandle.USER_SYSTEM; + boolean isMainUser = userId == getMainUserId(); boolean wipeDevice; if (factoryReset == null || !mInjector.isChangeEnabled(EXPLICIT_WIPE_BEHAVIOUR, adminPackage, userId)) { // Legacy mode - wipeDevice = isSystemUser; + wipeDevice = Flags.headlessSingleUserFixes() + && getHeadlessDeviceOwnerModeForDeviceOwner() + == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER ? isMainUser : isSystemUser; } else { // Explicit behaviour if (factoryReset) { @@ -8185,6 +8203,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { userHandle, /* parent= */ false); int max = strictestAdmin != null ? strictestAdmin.maximumFailedPasswordsForWipe : 0; + if (max > 0 && policy.mFailedPasswordAttempts >= max) { wipeData = true; } @@ -18398,6 +18417,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) || isProfileOwner(caller) || isFinancedDeviceOwner(caller)); + // Backup service has to be enabled on the main user in order for it to be enabled on + // secondary users. + if (Flags.headlessSingleUserFixes() && isDeviceOwner(caller) + && getHeadlessDeviceOwnerModeForDeviceOwner() + == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER) { + toggleBackupServiceActive(UserHandle.USER_SYSTEM, enabled); + } + toggleBackupServiceActive(caller.getUserId(), enabled); if (Flags.backupServiceSecurityLogEventEnabled()) { @@ -21745,7 +21772,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Objects.requireNonNull(deviceAdmin, "admin is null."); Objects.requireNonNull(provisioningParams.getOwnerName(), "owner name is null."); - final CallerIdentity caller = getCallerIdentity(); + final CallerIdentity caller = getCallerIdentity(callerPackage); Preconditions.checkCallAuthorization( hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS) || (hasCallingOrSelfPermission(permission.PROVISION_DEMO_DEVICE) @@ -21755,6 +21782,23 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final long identity = Binder.clearCallingIdentity(); try { + boolean isSingleUserMode; + if (Flags.headlessDeviceOwnerProvisioningFixEnabled()) { + int headlessDeviceOwnerMode = getHeadlessDeviceOwnerModeForDeviceAdmin( + deviceAdmin, caller.getUserId()); + isSingleUserMode = + headlessDeviceOwnerMode == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; + } else { + isSingleUserMode = + getHeadlessDeviceOwnerModeForDeviceOwner() + == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; + } + + if (Flags.headlessSingleUserFixes() && isSingleUserMode && !mInjector.isChangeEnabled( + PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(), caller.getUserId())) { + throw new IllegalStateException("Device admin is not targeting Android V."); + } + int result = checkProvisioningPreconditionSkipPermission( ACTION_PROVISION_MANAGED_DEVICE, deviceAdmin, caller.getUserId()); if (result != STATUS_OK) { @@ -21768,17 +21812,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { setTimeAndTimezone(provisioningParams.getTimeZone(), provisioningParams.getLocalTime()); setLocale(provisioningParams.getLocale()); - boolean isSingleUserMode; - if (Flags.headlessDeviceOwnerProvisioningFixEnabled()) { - int headlessDeviceOwnerMode = getHeadlessDeviceOwnerModeForDeviceAdmin( - deviceAdmin, caller.getUserId()); - isSingleUserMode = - headlessDeviceOwnerMode == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; - } else { - isSingleUserMode = - getHeadlessDeviceOwnerModeForDeviceOwner() - == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; - } int deviceOwnerUserId = Flags.headlessDeviceOwnerSingleUserEnabled() && isSingleUserMode ? mUserManagerInternal.getMainUserId() : UserHandle.USER_SYSTEM; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 43f24750ddef..5adfafb8a57c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -322,6 +322,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeEventLogger.reset(); } + private enum ChangeOrigin { + ORIGIN_UNKNOWN(ZenModeConfig.UPDATE_ORIGIN_UNKNOWN), + ORIGIN_INIT(ZenModeConfig.UPDATE_ORIGIN_INIT), + ORIGIN_INIT_USER(ZenModeConfig.UPDATE_ORIGIN_INIT_USER), + ORIGIN_USER(ZenModeConfig.UPDATE_ORIGIN_USER), + ORIGIN_APP(ZenModeConfig.UPDATE_ORIGIN_APP), + ORIGIN_SYSTEM_OR_SYSTEMUI(ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI), + ORIGIN_RESTORE_BACKUP(ZenModeConfig.UPDATE_ORIGIN_RESTORE_BACKUP); + + private final int mValue; + + ChangeOrigin(@ZenModeConfig.ConfigChangeOrigin int value) { + mValue = value; + } + + @ZenModeConfig.ConfigChangeOrigin + public int value() { + return mValue; + } + } + private XmlResourceParser getDefaultConfigParser() throws IOException, XmlPullParserException { String xml = "<zen version=\"10\">\n" + "<allow alarms=\"true\" media=\"true\" system=\"false\" calls=\"true\" " @@ -2898,6 +2919,72 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + @DisableFlags(Flags.FLAG_MODES_UI) + public void setManualZenMode_off_snoozesActiveRules(@TestParameter ChangeOrigin setZenOrigin) { + // Start with an active rule and an inactive rule. + mZenModeHelper.mConfig.automaticRules.clear(); + AutomaticZenRule activeRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + String activeRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + activeRule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(activeRuleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + CUSTOM_PKG_UID); + AutomaticZenRule inactiveRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + String inactiveRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + inactiveRule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + // User turns DND off. + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, setZenOrigin.value(), + "snoozing", "systemui", Process.SYSTEM_UID); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); + assertThat(mZenModeHelper.mConfig.automaticRules.get(activeRuleId).snoozing).isTrue(); + assertThat(mZenModeHelper.mConfig.automaticRules.get(inactiveRuleId).snoozing).isFalse(); + } + + @Test + @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + public void setManualZenMode_off_doesNotSnoozeRulesIfFromUser( + @TestParameter ChangeOrigin setZenOrigin) { + // Start with an active rule and an inactive rule + mZenModeHelper.mConfig.automaticRules.clear(); + AutomaticZenRule activeRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + String activeRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + activeRule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(activeRuleId, CONDITION_TRUE, UPDATE_ORIGIN_APP, + CUSTOM_PKG_UID); + AutomaticZenRule inactiveRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + String inactiveRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + inactiveRule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + // User turns DND off. + mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, setZenOrigin.value(), + "snoozing", "systemui", Process.SYSTEM_UID); + ZenModeConfig config = mZenModeHelper.mConfig; + if (setZenOrigin == ChangeOrigin.ORIGIN_USER) { + // Other rule was unaffected. + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertThat(config.automaticRules.get(activeRuleId).snoozing).isFalse(); + assertThat(config.automaticRules.get(inactiveRuleId).snoozing).isFalse(); + } else { + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); + assertThat(config.automaticRules.get(activeRuleId).snoozing).isTrue(); + assertThat(config.automaticRules.get(inactiveRuleId).snoozing).isFalse(); + } + } + + @Test public void testSetManualZenMode_legacy() { setupZenConfig(); |