diff options
145 files changed, 3560 insertions, 1156 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 17682a5b655a..52442a66cf1d 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -1433,7 +1433,10 @@ public class JobInfo implements Parcelable { } /** - * Specify that this job should be delayed by the provided amount of time. + * Specify that this job should be delayed by the provided amount of time. The job may not + * run the instant the delay has elapsed. JobScheduler will start the job at an + * indeterminate time after the delay has elapsed. + * <p> * Because it doesn't make sense setting this property on a periodic job, doing so will * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. @@ -1449,9 +1452,11 @@ public class JobInfo implements Parcelable { /** * Set deadline which is the maximum scheduling latency. The job will be run by this - * deadline even if other requirements are not met. Because it doesn't make sense setting - * this property on a periodic job, doing so will throw an - * {@link java.lang.IllegalArgumentException} when + * deadline even if other requirements (including a delay set through + * {@link #setMinimumLatency(long)}) are not met. + * <p> + * Because it doesn't make sense setting this property on a periodic job, doing so will + * throw an {@link java.lang.IllegalArgumentException} when * {@link android.app.job.JobInfo.Builder#build()} is called. * @see JobInfo#getMaxExecutionDelayMillis() */ @@ -1465,6 +1470,7 @@ public class JobInfo implements Parcelable { * Set up the back-off/retry policy. * This defaults to some respectable values: {30 seconds, Exponential}. We cap back-off at * 5hrs. + * <p> * Note that trying to set a backoff criteria for a job with * {@link #setRequiresDeviceIdle(boolean)} will throw an exception when you call build(). * This is because back-off typically does not make sense for these types of jobs. See diff --git a/apex/jobscheduler/framework/java/android/app/job/JobService.java b/apex/jobscheduler/framework/java/android/app/job/JobService.java index fa7a2d362ffa..c251529ae0b1 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobService.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobService.java @@ -153,6 +153,10 @@ public abstract class JobService extends Service { * Once this method returns (or times out), the system releases the wakelock that it is holding * on behalf of the job.</p> * + * <p class="caution"><strong>Note:</strong> When a job is stopped and rescheduled via this + * method call, the deadline constraint is excluded from the rescheduled job's constraint set. + * The rescheduled job will run again once all remaining constraints are satisfied. + * * @param params The parameters identifying this job, similar to what was supplied to the job in * the {@link #onStartJob(JobParameters)} callback, but with the stop reason * included. diff --git a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java index a9ca5cf5a26a..caf7e7f4a4ed 100644 --- a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java @@ -96,4 +96,13 @@ public interface DeviceIdleInternal { * that the device is stationary or in motion. */ void unregisterStationaryListener(StationaryListener listener); + + /** + * Apply some restrictions on temp allowlist type based on the reasonCode. + * @param reasonCode temp allowlist reason code. + * @param defaultType default temp allowlist type if reasonCode can not decide a type. + * @return temp allowlist type based on the reasonCode. + */ + @TempAllowListType int getTempAllowListType(@ReasonCode int reasonCode, + @TempAllowListType int defaultType); } diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 57c8300b66f6..60f5769a46f7 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -19,6 +19,7 @@ package com.android.server; import static android.os.PowerExemptionManager.REASON_SHELL; import static android.os.PowerExemptionManager.REASON_UNKNOWN; import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; +import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_NONE; import static android.os.Process.INVALID_UID; import android.Manifest; @@ -58,6 +59,7 @@ import android.os.Handler; import android.os.IDeviceIdleController; import android.os.Looper; import android.os.Message; +import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; import android.os.PowerManager; @@ -2015,6 +2017,12 @@ public class DeviceIdleController extends SystemService public void unregisterStationaryListener(StationaryListener listener) { DeviceIdleController.this.unregisterStationaryListener(listener); } + + @Override + public @TempAllowListType int getTempAllowListType(@ReasonCode int reasonCode, + @TempAllowListType int defaultType) { + return DeviceIdleController.this.getTempAllowListType(reasonCode, defaultType); + } } private class LocalPowerAllowlistService implements PowerAllowlistInternal { @@ -2689,6 +2697,18 @@ public class DeviceIdleController extends SystemService } } + private @TempAllowListType int getTempAllowListType(@ReasonCode int reasonCode, + @TempAllowListType int defaultType) { + switch (reasonCode) { + case PowerExemptionManager.REASON_PUSH_MESSAGING_OVER_QUOTA: + return mLocalActivityManager.getPushMessagingOverQuotaBehavior(); + case PowerExemptionManager.REASON_DENIED: + return TEMPORARY_ALLOW_LIST_TYPE_NONE; + default: + return defaultType; + } + } + void addPowerSaveTempAllowlistAppChecked(String packageName, long duration, int userId, @ReasonCode int reasonCode, @Nullable String reason) throws RemoteException { @@ -2705,9 +2725,12 @@ public class DeviceIdleController extends SystemService "addPowerSaveTempWhitelistApp", null); final long token = Binder.clearCallingIdentity(); try { - addPowerSaveTempAllowlistAppInternal(callingUid, - packageName, duration, TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED, - userId, true, reasonCode, reason); + @TempAllowListType int type = getTempAllowListType(reasonCode, + TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED); + if (type != TEMPORARY_ALLOW_LIST_TYPE_NONE) { + addPowerSaveTempAllowlistAppInternal(callingUid, + packageName, duration, type, userId, true, reasonCode, reason); + } } finally { Binder.restoreCallingIdentity(token); } @@ -2741,16 +2764,6 @@ public class DeviceIdleController extends SystemService void addPowerSaveTempAllowlistAppInternal(int callingUid, String packageName, long durationMs, @TempAllowListType int tempAllowListType, int userId, boolean sync, @ReasonCode int reasonCode, @Nullable String reason) { - synchronized (this) { - int callingAppId = UserHandle.getAppId(callingUid); - if (callingAppId >= Process.FIRST_APPLICATION_UID) { - if (!mPowerSaveWhitelistSystemAppIds.get(callingAppId)) { - throw new SecurityException( - "Calling app " + UserHandle.formatUid(callingUid) - + " is not on whitelist"); - } - } - } try { int uid = getContext().getPackageManager().getPackageUidAsUser(packageName, userId); addPowerSaveTempWhitelistAppDirectInternal(callingUid, uid, durationMs, diff --git a/cmds/sm/src/com/android/commands/sm/Sm.java b/cmds/sm/src/com/android/commands/sm/Sm.java index 260c8a47ea3c..f5bee6c0c724 100644 --- a/cmds/sm/src/com/android/commands/sm/Sm.java +++ b/cmds/sm/src/com/android/commands/sm/Sm.java @@ -258,7 +258,7 @@ public final class Sm { public void runDisableAppDataIsolation() throws RemoteException { if (!SystemProperties.getBoolean( - ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, false)) { + ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, true)) { throw new IllegalStateException("Storage app data isolation is not enabled."); } final String pkgName = nextArg(); diff --git a/core/api/current.txt b/core/api/current.txt index bdeb2e2bc2c3..a37203357cec 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -7466,9 +7466,10 @@ package android.app.admin { field public static final int LOCK_TASK_FEATURE_SYSTEM_INFO = 1; // 0x1 field public static final int MAKE_USER_EPHEMERAL = 2; // 0x2 field public static final String MIME_TYPE_PROVISIONING_NFC = "application/com.android.managedprovisioning"; - field public static final int NEARBY_STREAMING_DISABLED = 0; // 0x0 - field public static final int NEARBY_STREAMING_ENABLED = 1; // 0x1 - field public static final int NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY = 2; // 0x2 + field public static final int NEARBY_STREAMING_DISABLED = 1; // 0x1 + field public static final int NEARBY_STREAMING_ENABLED = 2; // 0x2 + field public static final int NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY = 0; // 0x0 + field public static final int NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY = 3; // 0x3 field public static final int OPERATION_SAFETY_REASON_DRIVING_DISTRACTION = 1; // 0x1 field public static final int PASSWORD_COMPLEXITY_HIGH = 327680; // 0x50000 field public static final int PASSWORD_COMPLEXITY_LOW = 65536; // 0x10000 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ed141780b166..c7960dd406ed 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -850,7 +850,7 @@ package android.app { field public static final int DEFAULT_PRIORITY = 0; // 0x0 field public static final String EXTRA_CALLING_PACKAGE = "android.app.extra.CALLING_PACKAGE"; field public static final String EXTRA_PRIORITY = "android.app.extra.PRIORITY"; - field public static final int PROJECTION_TYPE_ALL = 65535; // 0xffff + field public static final int PROJECTION_TYPE_ALL = -1; // 0xffffffff field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1 field public static final int PROJECTION_TYPE_NONE = 0; // 0x0 } @@ -10783,10 +10783,6 @@ package android.telecom { method public final void addExistingConnection(@NonNull android.telecom.PhoneAccountHandle, @NonNull android.telecom.Connection, @NonNull android.telecom.Conference); } - @Deprecated public abstract class DiagnosticCall extends android.telecom.CallDiagnostics { - ctor @Deprecated public DiagnosticCall(); - } - public abstract class InCallService extends android.app.Service { method @Deprecated public android.telecom.Phone getPhone(); method @Deprecated public void onPhoneCreated(android.telecom.Phone); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 61a9954c6336..875a9e7cd2e0 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -391,7 +391,7 @@ package android.app { method public boolean isUiModeLocked(); method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean releaseProjection(int); method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean requestProjection(int); - field public static final int PROJECTION_TYPE_ALL = 65535; // 0xffff + field public static final int PROJECTION_TYPE_ALL = -1; // 0xffffffff field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1 field public static final int PROJECTION_TYPE_NONE = 0; // 0x0 } diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 605340061994..ab610e4e71c6 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -583,4 +583,9 @@ public abstract class ActivityManagerInternal { * Is the FGS started from an uid temporarily allowed to have while-in-use permission? */ public abstract boolean isTempAllowlistedForFgsWhileInUse(int uid); + + /** + * Return the temp allowlist type when server push messaging is over the quota. + */ + public abstract @TempAllowListType int getPushMessagingOverQuotaBehavior(); } diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 9b99ab8e31cb..24fd04bedeae 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -713,7 +713,7 @@ public class UiModeManager { */ @SystemApi @TestApi - public static final int PROJECTION_TYPE_ALL = 0xffff; + public static final int PROJECTION_TYPE_ALL = -1; // All bits on /** @hide */ @IntDef(prefix = {"PROJECTION_TYPE_"}, value = { diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index cbf2d6a12bec..f07f45389d82 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -1678,23 +1678,30 @@ public class DevicePolicyManager { }) public @interface PasswordComplexity {} + /** + * Indicates that nearby streaming is not controlled by policy, which means nearby streaming is + * allowed. + */ + public static final int NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY = 0; + /** Indicates that nearby streaming is disabled. */ - public static final int NEARBY_STREAMING_DISABLED = 0; + public static final int NEARBY_STREAMING_DISABLED = 1; /** Indicates that nearby streaming is enabled. */ - public static final int NEARBY_STREAMING_ENABLED = 1; + public static final int NEARBY_STREAMING_ENABLED = 2; /** * Indicates that nearby streaming is enabled only to devices offering a comparable level of * security, with the same authenticated managed account. */ - public static final int NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY = 2; + public static final int NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY = 3; /** * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"NEARBY_STREAMING_"}, value = { + NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY, NEARBY_STREAMING_DISABLED, NEARBY_STREAMING_ENABLED, NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY, @@ -7199,15 +7206,20 @@ public class DevicePolicyManager { /** * Returns the current runtime nearby notification streaming policy set by the device or profile - * owner. The default is {@link #NEARBY_STREAMING_DISABLED}. + * owner. */ public @NearbyStreamingPolicy int getNearbyNotificationStreamingPolicy() { + return getNearbyNotificationStreamingPolicy(myUserId()); + } + + /** @hide per-user version */ + public @NearbyStreamingPolicy int getNearbyNotificationStreamingPolicy(int userId) { throwIfParentInstance("getNearbyNotificationStreamingPolicy"); if (mService == null) { - return NEARBY_STREAMING_DISABLED; + return NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; } try { - return mService.getNearbyNotificationStreamingPolicy(); + return mService.getNearbyNotificationStreamingPolicy(userId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } @@ -7235,15 +7247,19 @@ public class DevicePolicyManager { /** * Returns the current runtime nearby app streaming policy set by the device or profile owner. - * The default is {@link #NEARBY_STREAMING_DISABLED}. */ public @NearbyStreamingPolicy int getNearbyAppStreamingPolicy() { + return getNearbyAppStreamingPolicy(myUserId()); + } + + /** @hide per-user version */ + public @NearbyStreamingPolicy int getNearbyAppStreamingPolicy(int userId) { throwIfParentInstance("getNearbyAppStreamingPolicy"); if (mService == null) { - return NEARBY_STREAMING_DISABLED; + return NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; } try { - return mService.getNearbyAppStreamingPolicy(); + return mService.getNearbyAppStreamingPolicy(userId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 8e86f6545f23..370db60fb825 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -134,10 +134,10 @@ interface IDevicePolicyManager { boolean getScreenCaptureDisabled(in ComponentName who, int userHandle, boolean parent); void setNearbyNotificationStreamingPolicy(int policy); - int getNearbyNotificationStreamingPolicy(); + int getNearbyNotificationStreamingPolicy(int userId); void setNearbyAppStreamingPolicy(int policy); - int getNearbyAppStreamingPolicy(); + int getNearbyAppStreamingPolicy(int userId); void setKeyguardDisabledFeatures(in ComponentName who, int which, boolean parent); int getKeyguardDisabledFeatures(in ComponentName who, int userHandle, boolean parent); diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index 11adfa3cecc0..d41cda102103 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -100,6 +100,10 @@ public class ClipboardManager extends android.text.ClipboardManager { /** * Callback that is invoked by {@link android.content.ClipboardManager} when the primary * clip changes. + * + * <p>This is called when the result of {@link ClipDescription#getClassificationStatus()} + * changes, as well as when new clip data is set. So in cases where text classification is + * performed, this callback may be invoked multiple times for the same clip. */ void onPrimaryClipChanged(); } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 5e72325eed43..c2ac80e7c98f 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -310,8 +310,8 @@ interface IPackageManager { void restorePreferredActivities(in byte[] backup, int userId); byte[] getDefaultAppsBackup(int userId); void restoreDefaultApps(in byte[] backup, int userId); - byte[] getIntentFilterVerificationBackup(int userId); - void restoreIntentFilterVerification(in byte[] backup, int userId); + byte[] getDomainVerificationBackup(int userId); + void restoreDomainVerification(in byte[] backup, int userId); /** * Report the set of 'Home' activity candidates, plus (if any) which of them diff --git a/core/java/android/graphics/fonts/FontUpdateRequest.java b/core/java/android/graphics/fonts/FontUpdateRequest.java index 4dd5a72d446e..cda1638a24dc 100644 --- a/core/java/android/graphics/fonts/FontUpdateRequest.java +++ b/core/java/android/graphics/fonts/FontUpdateRequest.java @@ -147,7 +147,7 @@ public final class FontUpdateRequest implements Parcelable { public static Font readFromXml(XmlPullParser parser) throws IOException { String psName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME); if (psName == null) { - throw new IOException("name attribute is missing font tag."); + throw new IOException("name attribute is missing in font tag."); } int index = getAttributeValueInt(parser, ATTR_INDEX, 0); int weight = getAttributeValueInt(parser, ATTR_WEIGHT, FontStyle.FONT_WEIGHT_NORMAL); @@ -210,7 +210,7 @@ public final class FontUpdateRequest implements Parcelable { private static final String ATTR_NAME = "name"; private static final String TAG_FONT = "font"; - private final @Nullable String mName; + private final @NonNull String mName; private final @NonNull List<Font> mFonts; public Family(String name, List<Font> fonts) { @@ -281,6 +281,9 @@ public final class FontUpdateRequest implements Parcelable { throw new IOException("Unexpected parser state: must be START_TAG with family"); } String name = parser.getAttributeValue(null, ATTR_NAME); + if (name == null) { + throw new IOException("name attribute is missing in family tag."); + } int type = 0; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_FONT)) { diff --git a/core/java/android/hardware/display/BrightnessInfo.java b/core/java/android/hardware/display/BrightnessInfo.java index 4289860d4e8a..c5d37c2d0b90 100644 --- a/core/java/android/hardware/display/BrightnessInfo.java +++ b/core/java/android/hardware/display/BrightnessInfo.java @@ -33,7 +33,8 @@ public final class BrightnessInfo implements Parcelable { @IntDef(prefix = {"HIGH_BRIGHTNESS_MODE_"}, value = { HIGH_BRIGHTNESS_MODE_OFF, - HIGH_BRIGHTNESS_MODE_SUNLIGHT + HIGH_BRIGHTNESS_MODE_SUNLIGHT, + HIGH_BRIGHTNESS_MODE_HDR }) @Retention(RetentionPolicy.SOURCE) public @interface HighBrightnessMode {} @@ -50,6 +51,12 @@ public final class BrightnessInfo implements Parcelable { */ public static final int HIGH_BRIGHTNESS_MODE_SUNLIGHT = 1; + /** + * High brightness mode is ON due to high ambient light (sunlight). The high brightness range is + * currently accessible to the user. + */ + public static final int HIGH_BRIGHTNESS_MODE_HDR = 2; + /** Brightness */ public final float brightness; @@ -73,6 +80,21 @@ public final class BrightnessInfo implements Parcelable { this.highBrightnessMode = highBrightnessMode; } + /** + * @return User-friendly string for specified {@link HighBrightnessMode} parameter. + */ + public static String hbmToString(@HighBrightnessMode int highBrightnessMode) { + switch (highBrightnessMode) { + case HIGH_BRIGHTNESS_MODE_OFF: + return "off"; + case HIGH_BRIGHTNESS_MODE_HDR: + return "hdr"; + case HIGH_BRIGHTNESS_MODE_SUNLIGHT: + return "sunlight"; + } + return "invalid"; + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/view/OnReceiveContentListener.java b/core/java/android/view/OnReceiveContentListener.java index 3d9968c7f2c6..1e930e61c3bb 100644 --- a/core/java/android/view/OnReceiveContentListener.java +++ b/core/java/android/view/OnReceiveContentListener.java @@ -74,13 +74,17 @@ public interface OnReceiveContentListener { * preserve the common behavior for inserting text. See the class javadoc for a sample * implementation. * - * <p>If implementing handling for text: if the view has a selection, the selection should - * be overwritten by the passed-in content; if there's no selection, the passed-in content - * should be inserted at the current cursor position. - * - * <p>If implementing handling for non-text content (e.g. images): the content may be - * inserted inline, or it may be added as an attachment (could potentially be shown in a - * completely separate view). + * <p>Handling different content + * <ul> + * <li>Text. If the {@link ContentInfo#getSource() source} is + * {@link ContentInfo#SOURCE_AUTOFILL autofill}, the view's content should be fully + * replaced by the passed-in text. For sources other than autofill, the passed-in text + * should overwrite the current selection or be inserted at the current cursor position + * if there is no selection. + * <li>Non-text content (e.g. images). The content may be inserted inline if the widget + * supports this, or it may be added as an attachment (could potentially be shown in a + * completely separate view). + * </ul> * * @param view The view where the content insertion was requested. * @param payload The content to insert and related metadata. diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java index d84175ed1e7c..5b0abd389e7d 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTarget.java @@ -24,6 +24,7 @@ import static com.android.internal.accessibility.util.ShortcutUtils.optInValueTo import static com.android.internal.accessibility.util.ShortcutUtils.optOutValueFromSettings; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.graphics.drawable.Drawable; import android.view.View; @@ -33,6 +34,7 @@ import android.view.accessibility.AccessibilityManager.ShortcutType; import com.android.internal.accessibility.common.ShortcutConstants; import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; import com.android.internal.accessibility.dialog.TargetAdapter.ViewHolder; +import com.android.internal.annotations.VisibleForTesting; /** * Abstract base class for creating various target related to accessibility service, @@ -51,7 +53,8 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS private Drawable mIcon; private String mKey; - AccessibilityTarget(Context context, @ShortcutType int shortcutType, + @VisibleForTesting + public AccessibilityTarget(Context context, @ShortcutType int shortcutType, @AccessibilityFragmentType int fragmentType, boolean isShortcutSwitched, String id, CharSequence label, Drawable icon, String key) { mContext = context; @@ -103,6 +106,16 @@ public abstract class AccessibilityTarget implements TargetOperations, OnTargetS } } + /** + * Gets the state description of this feature target. + * + * @return the state description + */ + @Nullable + public CharSequence getStateDescription() { + return null; + } + public void setShortcutEnabled(boolean enabled) { mShortcutEnabled = enabled; } diff --git a/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java index 239e531dbfb8..469d10ff98aa 100644 --- a/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/ToggleAccessibilityServiceTarget.java @@ -51,10 +51,14 @@ class ToggleAccessibilityServiceTarget extends AccessibilityServiceTarget { final boolean isEditMenuMode = shortcutMenuMode == ShortcutMenuMode.EDIT; holder.mStatusView.setVisibility(isEditMenuMode ? View.GONE : View.VISIBLE); + holder.mStatusView.setText(getStateDescription()); + } + @Override + public CharSequence getStateDescription() { final int statusResId = isAccessibilityServiceEnabled(getContext(), getId()) ? R.string.accessibility_shortcut_menu_item_status_on : R.string.accessibility_shortcut_menu_item_status_off; - holder.mStatusView.setText(getContext().getString(statusResId)); + return getContext().getString(statusResId); } } diff --git a/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java b/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java index 38aac708de15..ebdaed6dbe39 100644 --- a/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java +++ b/core/java/com/android/internal/accessibility/dialog/ToggleAllowListingFeatureTarget.java @@ -48,11 +48,15 @@ class ToggleAllowListingFeatureTarget extends AccessibilityTarget { final boolean isEditMenuMode = shortcutMenuMode == ShortcutMenuMode.EDIT; holder.mStatusView.setVisibility(isEditMenuMode ? View.GONE : View.VISIBLE); + holder.mStatusView.setText(getStateDescription()); + } + @Override + public CharSequence getStateDescription() { final int statusResId = isFeatureEnabled() ? R.string.accessibility_shortcut_menu_item_status_on : R.string.accessibility_shortcut_menu_item_status_off; - holder.mStatusView.setText(getContext().getString(statusResId)); + return getContext().getString(statusResId); } private boolean isFeatureEnabled() { diff --git a/core/java/com/android/server/backup/PreferredActivityBackupHelper.java b/core/java/com/android/server/backup/PreferredActivityBackupHelper.java index 80636706a9a6..503c71990adb 100644 --- a/core/java/com/android/server/backup/PreferredActivityBackupHelper.java +++ b/core/java/com/android/server/backup/PreferredActivityBackupHelper.java @@ -16,10 +16,12 @@ package com.android.server.backup; +import android.annotation.StringDef; +import android.annotation.UserIdInt; import android.app.AppGlobals; import android.app.backup.BlobBackupHelper; import android.content.pm.IPackageManager; -import android.os.UserHandle; +import android.content.pm.verify.domain.DomainVerificationManager; import android.util.Slog; public class PreferredActivityBackupHelper extends BlobBackupHelper { @@ -27,7 +29,7 @@ public class PreferredActivityBackupHelper extends BlobBackupHelper { private static final boolean DEBUG = false; // current schema of the backup state blob - private static final int STATE_VERSION = 3; + private static final int STATE_VERSION = 4; // key under which the preferred-activity state blob is committed to backup private static final String KEY_PREFERRED = "preferred-activity"; @@ -35,14 +37,41 @@ public class PreferredActivityBackupHelper extends BlobBackupHelper { // key for default-browser [etc] state private static final String KEY_DEFAULT_APPS = "default-apps"; - // intent-filter verification state + /** + * Intent-filter verification state + * @deprecated Replaced by {@link #KEY_DOMAIN_VERIFICATION}, retained to ensure the key is + * never reused. + */ + @Deprecated private static final String KEY_INTENT_VERIFICATION = "intent-verification"; - public PreferredActivityBackupHelper() { - super(STATE_VERSION, - KEY_PREFERRED, - KEY_DEFAULT_APPS, - KEY_INTENT_VERIFICATION); + /** + * State for {@link DomainVerificationManager}. + */ + private static final String KEY_DOMAIN_VERIFICATION = "domain-verification"; + + private static final String[] KEYS = new String[] { + KEY_PREFERRED, + KEY_DEFAULT_APPS, + KEY_INTENT_VERIFICATION, + KEY_DOMAIN_VERIFICATION + }; + + @StringDef(value = { + KEY_PREFERRED, + KEY_DEFAULT_APPS, + KEY_INTENT_VERIFICATION, + KEY_DOMAIN_VERIFICATION + }) + private @interface Key { + } + + @UserIdInt + private final int mUserId; + + public PreferredActivityBackupHelper(@UserIdInt int userId) { + super(STATE_VERSION, KEYS); + mUserId = userId; } @Override @@ -52,14 +81,16 @@ public class PreferredActivityBackupHelper extends BlobBackupHelper { Slog.d(TAG, "Handling backup of " + key); } try { - // TODO: http://b/22388012 switch (key) { case KEY_PREFERRED: - return pm.getPreferredActivityBackup(UserHandle.USER_SYSTEM); + return pm.getPreferredActivityBackup(mUserId); case KEY_DEFAULT_APPS: - return pm.getDefaultAppsBackup(UserHandle.USER_SYSTEM); + return pm.getDefaultAppsBackup(mUserId); case KEY_INTENT_VERIFICATION: - return pm.getIntentFilterVerificationBackup(UserHandle.USER_SYSTEM); + // Deprecated + return null; + case KEY_DOMAIN_VERIFICATION: + return pm.getDomainVerificationBackup(mUserId); default: Slog.w(TAG, "Unexpected backup key " + key); } @@ -70,22 +101,24 @@ public class PreferredActivityBackupHelper extends BlobBackupHelper { } @Override - protected void applyRestoredPayload(String key, byte[] payload) { + protected void applyRestoredPayload(@Key String key, byte[] payload) { IPackageManager pm = AppGlobals.getPackageManager(); if (DEBUG) { Slog.d(TAG, "Handling restore of " + key); } try { - // TODO: http://b/22388012 switch (key) { case KEY_PREFERRED: - pm.restorePreferredActivities(payload, UserHandle.USER_SYSTEM); + pm.restorePreferredActivities(payload, mUserId); break; case KEY_DEFAULT_APPS: - pm.restoreDefaultApps(payload, UserHandle.USER_SYSTEM); + pm.restoreDefaultApps(payload, mUserId); break; case KEY_INTENT_VERIFICATION: - pm.restoreIntentFilterVerification(payload, UserHandle.USER_SYSTEM); + // Deprecated + break; + case KEY_DOMAIN_VERIFICATION: + pm.restoreDomainVerification(payload, mUserId); break; default: Slog.w(TAG, "Unexpected restore key " + key); diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 8b7096646cff..fee82bac489c 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -825,7 +825,7 @@ static void MountEmulatedStorage(uid_t uid, jint mount_mode, PrepareDir(user_source, 0710, user_id ? AID_ROOT : AID_SHELL, multiuser_get_uid(user_id, AID_EVERYBODY), fail_fn); - bool isAppDataIsolationEnabled = GetBoolProperty(kVoldAppDataIsolation, false); + bool isAppDataIsolationEnabled = GetBoolProperty(kVoldAppDataIsolation, true); if (mount_mode == MOUNT_EXTERNAL_PASS_THROUGH) { const std::string pass_through_source = StringPrintf("/mnt/pass_through/%d", user_id); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index d4fa28531acd..480b47835100 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -3602,6 +3602,10 @@ <attr name="__removed2" format="boolean" /> <!-- Specifies whether the IME supports showing inline suggestions. --> <attr name="supportsInlineSuggestions" format="boolean" /> + <!-- Specifies whether the IME suppresses system spell checker. + The default value is false. If an IME sets this attribute to true, + the system spell checker will be disabled while the IME has an + active input session. --> <attr name="suppressesSpellChecker" format="boolean" /> <!-- Specifies whether the IME wants to be shown in the Input Method picker. Defaults to true. Set this to false if the IME is intended to be accessed programmatically. diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml index 8224d95fd9ad..270186a199bb 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_container.xml @@ -27,24 +27,26 @@ <androidx.recyclerview.widget.RecyclerView android:id="@+id/bubble_overflow_recycler" - android:layout_gravity="center_horizontal" android:nestedScrollingEnabled="false" - android:layout_width="wrap_content" - android:layout_height="wrap_content"/> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center_horizontal" + android:gravity="center"/> <LinearLayout android:id="@+id/bubble_overflow_empty_state" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="match_parent" android:paddingLeft="@dimen/bubble_overflow_empty_state_padding" android:paddingRight="@dimen/bubble_overflow_empty_state_padding" android:orientation="vertical" android:gravity="center"> <ImageView + android:id="@+id/bubble_overflow_empty_state_image" android:layout_width="@dimen/bubble_empty_overflow_image_height" android:layout_height="@dimen/bubble_empty_overflow_image_height" - android:id="@+id/bubble_overflow_empty_state_image" android:scaleType="fitCenter" android:layout_gravity="center"/> @@ -60,12 +62,12 @@ <TextView android:id="@+id/bubble_overflow_empty_subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" android:fontFamily="@*android:string/config_bodyFontFamily" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2" android:textColor="?android:attr/textColorSecondary" android:text="@string/bubble_overflow_empty_subtitle" - android:layout_width="match_parent" - android:layout_height="wrap_content" android:paddingBottom="@dimen/bubble_empty_overflow_subtitle_padding" android:gravity="center"/> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml index c1f67bd27d93..d07107c8f0a0 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml @@ -32,12 +32,14 @@ android:id="@+id/bubble_view_name" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" android:textSize="13sp" - android:layout_width="fill_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="1" android:lines="2" android:ellipsize="end" android:layout_gravity="center" android:paddingTop="@dimen/bubble_overflow_text_padding" + android:paddingEnd="@dimen/bubble_overflow_text_padding" + android:paddingStart="@dimen/bubble_overflow_text_padding" android:gravity="center"/> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index ef731235a3c4..d94030dba652 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -125,7 +125,9 @@ <dimen name="bubble_expanded_view_slop">8dp</dimen> <!-- Default (and minimum) height of the expanded view shown when the bubble is expanded --> <dimen name="bubble_expanded_default_height">180dp</dimen> - <!-- Default height of bubble overflow --> + <!-- On large screens the width of the expanded view is restricted to this size. --> + <dimen name="bubble_expanded_view_tablet_width">412dp</dimen> + <!-- Default (and minimum) height of bubble overflow --> <dimen name="bubble_overflow_height">480dp</dimen> <!-- Bubble overflow padding when there are no bubbles --> <dimen name="bubble_overflow_empty_state_padding">16dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index abe1f7179dda..696f705782c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; @@ -335,7 +336,10 @@ public class BubbleExpandedView extends LinearLayout { mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate( R.layout.bubble_overflow_container, null /* root */); mOverflowView.setBubbleController(mController); - mExpandedViewContainer.addView(mOverflowView); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); + mExpandedViewContainer.addView(mOverflowView, lp); + mExpandedViewContainer.setLayoutParams( + new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); bringChildToFront(mOverflowView); mSettingsIcon.setVisibility(GONE); } else { @@ -600,9 +604,9 @@ public class BubbleExpandedView extends LinearLayout { return; } - if (mBubble != null || mIsOverflow) { + if ((mBubble != null && mTaskView != null) || mIsOverflow) { float desiredHeight = mIsOverflow - ? mOverflowHeight + ? mPositioner.isLargeScreen() ? getMaxExpandedHeight() : mOverflowHeight : mBubble.getDesiredHeight(mContext); desiredHeight = Math.max(desiredHeight, mMinHeight); float height = Math.min(desiredHeight, getMaxExpandedHeight()); @@ -657,10 +661,10 @@ public class BubbleExpandedView extends LinearLayout { + getBubbleKey()); } mExpandedViewContainerLocation = containerLocationOnScreen; + updateHeight(); if (mTaskView != null && mTaskView.getVisibility() == VISIBLE && mTaskView.isAttachedToWindow()) { - updateHeight(); mTaskView.onLocationChanged(); } if (mIsOverflow) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java index f7c7285a7b6e..af5b3a61f393 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java @@ -26,7 +26,6 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -51,8 +50,6 @@ import java.util.function.Consumer; * Container view for showing aged out bubbles. */ public class BubbleOverflowContainerView extends LinearLayout { - static final String EXTRA_BUBBLE_CONTROLLER = "bubble_controller"; - private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES; private LinearLayout mEmptyState; @@ -64,18 +61,16 @@ public class BubbleOverflowContainerView extends LinearLayout { private RecyclerView mRecyclerView; private List<Bubble> mOverflowBubbles = new ArrayList<>(); - private class NoScrollGridLayoutManager extends GridLayoutManager { - NoScrollGridLayoutManager(Context context, int columns) { + private class OverflowGridLayoutManager extends GridLayoutManager { + OverflowGridLayoutManager(Context context, int columns) { super(context, columns); } - @Override - public boolean canScrollVertically() { - if (getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE) { - return super.canScrollVertically(); - } - return false; - } + +// @Override +// public boolean canScrollVertically() { +// // TODO (b/162006693): this should be based on items in the list & available height +// return true; +// } @Override public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, @@ -137,47 +132,24 @@ public class BubbleOverflowContainerView extends LinearLayout { Resources res = getResources(); final int columns = res.getInteger(R.integer.bubbles_overflow_columns); mRecyclerView.setLayoutManager( - new NoScrollGridLayoutManager(getContext(), columns)); - - DisplayMetrics displayMetrics = new DisplayMetrics(); - getContext().getDisplay().getMetrics(displayMetrics); - - final int overflowPadding = res.getDimensionPixelSize(R.dimen.bubble_overflow_padding); - final int recyclerViewWidth = displayMetrics.widthPixels - (overflowPadding * 2); - final int viewWidth = recyclerViewWidth / columns; - - final int maxOverflowBubbles = res.getInteger(R.integer.bubbles_max_overflow); - final int rows = (int) Math.ceil((double) maxOverflowBubbles / columns); - final int recyclerViewHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height) - - res.getDimensionPixelSize(R.dimen.bubble_overflow_padding); - final int viewHeight = recyclerViewHeight / rows; - + new OverflowGridLayoutManager(getContext(), columns)); mAdapter = new BubbleOverflowAdapter(getContext(), mOverflowBubbles, mController::promoteBubbleFromOverflow, - mController.getPositioner(), - viewWidth, viewHeight); + mController.getPositioner()); mRecyclerView.setAdapter(mAdapter); mOverflowBubbles.clear(); mOverflowBubbles.addAll(mController.getOverflowBubbles()); mAdapter.notifyDataSetChanged(); - // Currently BubbleExpandedView.mExpandedViewContainer is WRAP_CONTENT so use the same - // width we would use for the recycler view - LayoutParams lp = (LayoutParams) mEmptyState.getLayoutParams(); - lp.width = recyclerViewWidth; - updateEmptyStateVisibility(); - mController.setOverflowListener(mDataListener); + updateEmptyStateVisibility(); updateTheme(); } void updateEmptyStateVisibility() { - if (mOverflowBubbles.isEmpty()) { - mEmptyState.setVisibility(View.VISIBLE); - } else { - mEmptyState.setVisibility(View.GONE); - } + mEmptyState.setVisibility(mOverflowBubbles.isEmpty() ? View.VISIBLE : View.GONE); + mRecyclerView.setVisibility(mOverflowBubbles.isEmpty() ? View.GONE : View.VISIBLE); } /** @@ -258,20 +230,15 @@ class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.V private Consumer<Bubble> mPromoteBubbleFromOverflow; private BubblePositioner mPositioner; private List<Bubble> mBubbles; - private int mWidth; - private int mHeight; BubbleOverflowAdapter(Context context, List<Bubble> list, Consumer<Bubble> promoteBubble, - BubblePositioner positioner, - int width, int height) { + BubblePositioner positioner) { mContext = context; mBubbles = list; mPromoteBubbleFromOverflow = promoteBubble; mPositioner = positioner; - mWidth = width; - mHeight = height; } @Override @@ -284,8 +251,6 @@ class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.V LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); - params.width = mWidth; - params.height = mHeight; overflowView.setLayoutParams(params); // Ensure name has enough contrast. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index a81c2d8bef0a..ae1a053ae19e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -70,10 +70,16 @@ public class BubblePositioner { private int mBubbleSize; private int mBubbleBitmapSize; + private int mExpandedViewLargeScreenWidth; + private int mExpandedViewPadding; + private int mPointerHeight; + private int mBubblePaddingTop; private PointF mPinLocation; private PointF mRestingStackPosition; + private int[] mLeftRightPadding = new int[2]; + private boolean mIsLargeScreen; private boolean mShowingInTaskbar; private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE; private int mTaskbarIconSize; @@ -99,15 +105,17 @@ public class BubblePositioner { return; } WindowInsets metricInsets = windowMetrics.getWindowInsets(); - Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars() | WindowInsets.Type.statusBars() | WindowInsets.Type.displayCutout()); + mIsLargeScreen = mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600; + if (BubbleDebugConfig.DEBUG_POSITIONER) { Log.w(TAG, "update positioner:" - + " rotation= " + mRotation + + " rotation: " + mRotation + " insets: " + insets + + " isLargeScreen: " + mIsLargeScreen + " bounds: " + windowMetrics.getBounds() + " showingInTaskbar: " + mShowingInTaskbar); } @@ -143,6 +151,11 @@ public class BubblePositioner { Resources res = mContext.getResources(); mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); mBubbleBitmapSize = res.getDimensionPixelSize(R.dimen.bubble_bitmap_size); + mExpandedViewLargeScreenWidth = res.getDimensionPixelSize( + R.dimen.bubble_expanded_view_tablet_width); + mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); + mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); + mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); if (mShowingInTaskbar) { adjustForTaskbar(); } @@ -189,13 +202,16 @@ public class BubblePositioner { return mInsets; } - /** - * @return whether the device is in landscape orientation. - */ + /** @return whether the device is in landscape orientation. */ public boolean isLandscape() { return mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270; } + /** @return whether the screen is considered large. */ + public boolean isLargeScreen() { + return mIsLargeScreen; + } + /** * Indicates how bubbles appear when expanded. * @@ -204,7 +220,7 @@ public class BubblePositioner { * to the left or right side. */ public boolean showBubblesVertically() { - return isLandscape() || mShowingInTaskbar; + return isLandscape() || mShowingInTaskbar || mIsLargeScreen; } /** Size of the bubble account for badge & dot. */ @@ -224,6 +240,45 @@ public class BubblePositioner { } /** + * Calculates the left & right padding for the bubble expanded view. + * + * On larger screens the width of the expanded view is restricted via this padding. + * On landscape the bubble overflow expanded view is also restricted via this padding. + */ + public int[] getExpandedViewPadding(boolean onLeft, boolean isOverflow) { + int leftPadding = mInsets.left + mExpandedViewPadding; + int rightPadding = mInsets.right + mExpandedViewPadding; + final boolean isLargeOrOverflow = mIsLargeScreen || isOverflow; + if (showBubblesVertically()) { + if (!onLeft) { + rightPadding += mPointerHeight + mBubbleSize; + leftPadding += isLargeOrOverflow + ? (mPositionRect.width() - rightPadding - mExpandedViewLargeScreenWidth) + : 0; + } else { + //TODO: pointer height should be padding between pointer & bubbles here & above + leftPadding += mPointerHeight + mBubbleSize; + rightPadding += isLargeOrOverflow + ? (mPositionRect.width() - leftPadding - mExpandedViewLargeScreenWidth) + : 0; + } + } + mLeftRightPadding[0] = leftPadding; + mLeftRightPadding[1] = rightPadding; + return mLeftRightPadding; + } + + /** Calculates the y position of the expanded view when it is expanded. */ + public float getExpandedViewY() { + final int top = getAvailableRect().top; + if (showBubblesVertically()) { + return top + mExpandedViewPadding; + } else { + return top + mBubbleSize + mBubblePaddingTop; + } + } + + /** * Sets the stack's most recent position along the edge of the screen. This is saved when the * last bubble is removed, so that the stack can be restored in its previous position. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 0f5d0eff7efa..c4d33877f17d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -35,7 +35,6 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; -import android.graphics.Insets; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PointF; @@ -246,7 +245,6 @@ public class BubbleStackView extends FrameLayout private int mMaxBubbles; private int mBubbleSize; private int mBubbleElevation; - private int mBubblePaddingTop; private int mBubbleTouchPadding; private int mExpandedViewPadding; private int mPointerHeight; @@ -768,7 +766,6 @@ public class BubbleStackView extends FrameLayout mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); @@ -905,7 +902,7 @@ public class BubbleStackView extends FrameLayout afterExpandedViewAnimation(); } /* after */); mExpandedViewContainer.setTranslationX(0f); - mExpandedViewContainer.setTranslationY(getExpandedViewY()); + mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); mExpandedViewContainer.setAlpha(1f); } removeOnLayoutChangeListener(mOrientationChangedListener); @@ -1247,9 +1244,6 @@ public class BubbleStackView extends FrameLayout /** Respond to the display size change by recalculating view size and location. */ public void onDisplaySizeChanged() { updateOverflow(); - - Resources res = getContext().getResources(); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mBubbleSize = mPositioner.getBubbleSize(); for (Bubble b : mBubbleData.getBubbles()) { if (b.getIconView() == null) { @@ -1267,6 +1261,9 @@ public class BubbleStackView extends FrameLayout new RelativeStackPosition( mPositioner.getRestingPosition(), mStackAnimationController.getAllowableStackPositionRegion())); + if (mIsExpanded) { + updateExpandedView(); + } } @Override @@ -1816,7 +1813,7 @@ public class BubbleStackView extends FrameLayout } mExpandedViewContainer.setTranslationX(0f); - mExpandedViewContainer.setTranslationY(getExpandedViewY()); + mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); mExpandedViewContainer.setAlpha(1f); int index; @@ -1865,7 +1862,7 @@ public class BubbleStackView extends FrameLayout 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, bubbleWillBeAt + mBubbleSize / 2f, - getExpandedViewY()); + mPositioner.getExpandedViewY()); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); @@ -1969,7 +1966,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainerMatrix.setScale( 1f, 1f, expandingFromBubbleAt + mBubbleSize / 2f, - getExpandedViewY()); + mPositioner.getExpandedViewY()); } mExpandedViewAlphaAnimator.reverse(); @@ -2075,7 +2072,7 @@ public class BubbleStackView extends FrameLayout 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, expandingFromBubbleDestination + mBubbleSize / 2f, - getExpandedViewY()); + mPositioner.getExpandedViewY()); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); @@ -2313,18 +2310,6 @@ public class BubbleStackView extends FrameLayout : 0f); } - /** - * Calculates the y position of the expanded view when it is expanded. - */ - float getExpandedViewY() { - final int top = mPositioner.getAvailableRect().top; - if (mPositioner.showBubblesVertically()) { - return top + mExpandedViewPadding; - } else { - return top + mBubbleSize + mBubblePaddingTop; - } - } - private boolean shouldShowFlyout(Bubble bubble) { Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage(); final BadgedImageView bubbleView = bubble.getIconView(); @@ -2697,24 +2682,16 @@ public class BubbleStackView extends FrameLayout if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded); } - - // Need to update the padding around the view for any insets - Insets insets = mPositioner.getInsets(); - int leftPadding = insets.left + mExpandedViewPadding; - int rightPadding = insets.right + mExpandedViewPadding; - if (mPositioner.showBubblesVertically()) { - if (!mStackAnimationController.isStackOnLeftSide()) { - rightPadding += mPointerHeight + mBubbleSize; - } else { - leftPadding += mPointerHeight + mBubbleSize; - } - } - mExpandedViewContainer.setPadding(leftPadding, 0, rightPadding, 0); + boolean isOverflowExpanded = mExpandedBubble != null + && mBubbleOverflow.KEY.equals(mExpandedBubble.getKey()); + int[] paddings = mPositioner.getExpandedViewPadding( + mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded); + mExpandedViewContainer.setPadding(paddings[0], 0, paddings[1], 0); if (mIsExpansionAnimating) { mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); } if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedViewContainer.setTranslationY(getExpandedViewY()); + mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); mExpandedViewContainer.setTranslationX(0f); mExpandedBubble.getExpandedView().updateView( mExpandedViewContainer.getLocationOnScreen()); @@ -2755,13 +2732,14 @@ public class BubbleStackView extends FrameLayout return; } float bubblePosition = mExpandedAnimationController.getBubbleXOrYForOrientation(index); + float expandedViewY = mPositioner.getExpandedViewY(); if (mPositioner.showBubblesVertically()) { float x = mStackOnLeftOrWillBe ? mPositioner.getAvailableRect().left : mPositioner.getAvailableRect().right - mExpandedViewContainer.getPaddingRight() - mPointerHeight; - float bubbleCenter = bubblePosition - getExpandedViewY() + (mBubbleSize / 2f); + float bubbleCenter = bubblePosition - expandedViewY + (mBubbleSize / 2f); mExpandedBubble.getExpandedView().setPointerPosition( x, bubbleCenter, @@ -2771,7 +2749,7 @@ public class BubbleStackView extends FrameLayout float bubbleCenter = bubblePosition + (mBubbleSize / 2f); mExpandedBubble.getExpandedView().setPointerPosition( bubbleCenter, - getExpandedViewY(), + expandedViewY, false, mStackOnLeftOrWillBe); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java index 18aaa9677be6..48bd8943b25a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -16,7 +16,6 @@ package com.android.wm.shell.bubbles.animation; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Path; import android.graphics.PointF; @@ -142,6 +141,7 @@ public class ExpandedAnimationController updateResources(); mExpandedViewPadding = expandedViewPadding; mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; + mCollapsePoint = mPositioner.getDefaultStartPosition(); } /** @@ -528,17 +528,34 @@ public class ExpandedAnimationController startOrUpdatePathAnimation(true /* expanding */); } else if (mAnimatingCollapse) { startOrUpdatePathAnimation(false /* expanding */); + } else if (mPositioner.showBubblesVertically()) { + child.setTranslationY(getBubbleXOrYForOrientation(index)); + if (!mPreparingToCollapse) { + // Only animate if we're not collapsing as that animation will handle placing the + // new bubble in the stacked position. + Rect availableRect = mPositioner.getAvailableRect(); + boolean onLeft = mCollapsePoint != null + && mCollapsePoint.x < (availableRect.width() / 2f); + float fromX = onLeft + ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR + : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; + float toX = onLeft + ? availableRect.left + mExpandedViewPadding + : availableRect.right - mBubbleSizePx - mExpandedViewPadding; + animationForChild(child) + .translationX(fromX, toX) + .start(); + updateBubblePositions(); + } } else { child.setTranslationX(getBubbleXOrYForOrientation(index)); - - // If we're preparing to collapse, don't start animations since the collapse animation - // will take over and animate the new bubble into the correct (stacked) position. if (!mPreparingToCollapse) { + // Only animate if we're not collapsing as that animation will handle placing the + // new bubble in the stacked position. + float toY = getExpandedY(); + float fromY = getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; animationForChild(child) - .translationY( - getExpandedY() - - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR, /* from */ - getExpandedY() /* to */) + .translationY(fromY, toY) .start(); updateBubblePositions(); } @@ -617,15 +634,16 @@ public class ExpandedAnimationController } } + // TODO - could move to method on bubblePositioner if mSpaceBetweenBubbles gets moved /** * When bubbles are expanded in portrait, they display at the top of the screen in a horizontal - * row. When in landscape, they show at the left or right side in a vertical row. This method - * accounts for screen orientation and will return an x or y value for the position of the - * bubble in the row. + * row. When in landscape or on a large screen, they show at the left or right side in a + * vertical row. This method accounts for screen orientation and will return an x or y value + * for the position of the bubble in the row. * * @param index Bubble index in row. - * @return the y position of the bubble if {@link Configuration#ORIENTATION_LANDSCAPE} and the - * x position if {@link Configuration#ORIENTATION_PORTRAIT}. + * @return the y position of the bubble if showing vertically and the x position if showing + * horizontally. */ public float getBubbleXOrYForOrientation(int index) { if (mLayout == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 56fe126f507e..578f87fbfbf8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -958,17 +958,26 @@ public class StackAnimationController extends if (!isActiveController()) { return; } - v.setTranslationX(mStackPosition.x); + final float yOffset = getOffsetForChainedPropertyAnimation(DynamicAnimation.TRANSLATION_Y); - final float endY = mStackPosition.y + yOffset * index; - final float startY = endY + NEW_BUBBLE_START_Y; - v.setTranslationY(startY); + float endY = mStackPosition.y + yOffset * index; + float endX = mStackPosition.x; + if (mPositioner.showBubblesVertically()) { + v.setTranslationY(endY); + final float startX = isStackOnLeftSide() + ? endX - NEW_BUBBLE_START_Y + : endX + NEW_BUBBLE_START_Y; + v.setTranslationX(startX); + } else { + v.setTranslationX(mStackPosition.x); + final float startY = endY + NEW_BUBBLE_START_Y; + v.setTranslationY(startY); + } v.setScaleX(NEW_BUBBLE_START_SCALE); v.setScaleY(NEW_BUBBLE_START_SCALE); v.setAlpha(0f); final ViewPropertyAnimator animator = v.animate() - .translationY(endY) .scaleX(1f) .scaleY(1f) .alpha(1f) @@ -977,6 +986,11 @@ public class StackAnimationController extends v.setTag(R.id.reorder_animator_tag, null); }); v.setTag(R.id.reorder_animator_tag, animator); + if (mPositioner.showBubblesVertically()) { + animator.translationX(endX); + } else { + animator.translationY(endY); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 17cde731faa2..9cf0b721cc48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -18,6 +18,8 @@ package com.android.wm.shell.pip.phone; import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; +import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_HIDE; + import android.annotation.Nullable; import android.app.RemoteAction; import android.content.Context; @@ -397,26 +399,26 @@ public class PhonePipMenuController implements PipMenuController { * Hides the menu view. */ public void hideMenu() { - hideMenu(true /* animate */, true /* resize */); + hideMenu(ANIM_TYPE_HIDE, true /* resize */); } /** * Hides the menu view. * - * @param animate whether to animate the menu fadeout + * @param animationType the animation type to use upon hiding the menu * @param resize whether or not to resize the PiP with the state change */ - public void hideMenu(boolean animate, boolean resize) { + public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) { final boolean isMenuVisible = isMenuVisible(); if (DEBUG) { Log.d(TAG, "hideMenu() state=" + mMenuState + " isMenuVisible=" + isMenuVisible - + " animate=" + animate + + " animationType=" + animationType + " resize=" + resize + " callers=\n" + Debug.getCallers(5, " ")); } if (isMenuVisible) { - mPipMenuView.hideMenu(animate, resize); + mPipMenuView.hideMenu(resize, animationType); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index a57e8cdd0928..2b45346cfa5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -32,6 +32,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.annotation.IntDef; import android.app.PendingIntent.CanceledException; import android.app.RemoteAction; import android.content.ComponentName; @@ -64,6 +65,8 @@ import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -74,9 +77,26 @@ public class PipMenuView extends FrameLayout { private static final String TAG = "PipMenuView"; + private static final int ANIMATION_NONE_DURATION_MS = 0; + private static final int ANIMATION_HIDE_DURATION_MS = 125; + + /** No animation performed during menu hide. */ + public static final int ANIM_TYPE_NONE = 0; + /** Fade out the menu until it's invisible. Used when the PIP window remains visible. */ + public static final int ANIM_TYPE_HIDE = 1; + /** Fade out the menu in sync with the PIP window. */ + public static final int ANIM_TYPE_DISMISS = 2; + + @IntDef(prefix = { "ANIM_TYPE_" }, value = { + ANIM_TYPE_NONE, + ANIM_TYPE_HIDE, + ANIM_TYPE_DISMISS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AnimationType {} + private static final int INITIAL_DISMISS_DELAY = 3500; private static final int POST_INTERACTION_DISMISS_DELAY = 2000; - private static final long MENU_FADE_DURATION = 125; private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30; private static final float MENU_BACKGROUND_ALPHA = 0.3f; @@ -87,6 +107,7 @@ public class PipMenuView extends FrameLayout { private int mMenuState; private boolean mAllowMenuTimeout = true; private boolean mAllowTouches = true; + private int mDismissFadeOutDurationMs; private final List<RemoteAction> mActions = new ArrayList<>(); @@ -167,6 +188,8 @@ public class PipMenuView extends FrameLayout { mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext); mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer, mResizeHandle, mSettingsButton, mDismissButton); + mDismissFadeOutDurationMs = context.getResources() + .getInteger(R.integer.config_pipExitAnimationDuration); initAccessibility(); } @@ -258,7 +281,7 @@ public class PipMenuView extends FrameLayout { mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim); } mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN); - mMenuContainerAnimator.setDuration(MENU_FADE_DURATION); + mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS); if (allowMenuTimeout) { mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { @Override @@ -320,17 +343,18 @@ public class PipMenuView extends FrameLayout { hideMenu(null); } - void hideMenu(boolean animate, boolean resize) { - hideMenu(null, true /* notifyMenuVisibility */, animate, resize); + void hideMenu(Runnable animationEndCallback) { + hideMenu(animationEndCallback, true /* notifyMenuVisibility */, true /* resize */, + ANIM_TYPE_HIDE); } - void hideMenu(Runnable animationEndCallback) { - hideMenu(animationEndCallback, true /* notifyMenuVisibility */, true /* animate */, - true /* resize */); + void hideMenu(boolean resize, @AnimationType int animationType) { + hideMenu(null /* animationFinishedRunnable */, true /* notifyMenuVisibility */, resize, + animationType); } - private void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, - boolean animate, boolean resize) { + void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, + boolean resize, @AnimationType int animationType) { if (mMenuState != MENU_STATE_NONE) { cancelDelayedHide(); if (notifyMenuVisibility) { @@ -348,7 +372,7 @@ public class PipMenuView extends FrameLayout { mResizeHandle.getAlpha(), 0f); mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, resizeAnim); mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT); - mMenuContainerAnimator.setDuration(animate ? MENU_FADE_DURATION : 0); + mMenuContainerAnimator.setDuration(getFadeOutDuration(animationType)); mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -478,19 +502,17 @@ public class PipMenuView extends FrameLayout { private void expandPip() { // Do not notify menu visibility when hiding the menu, the controller will do this when it // handles the message - hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* animate */, - true /* resize */); + hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* resize */, + ANIM_TYPE_HIDE); } private void dismissPip() { - // Since tapping on the close-button invokes a double-tap wait callback in PipTouchHandler, - // we want to disable animating the fadeout animation of the buttons in order to call on - // PipTouchHandler#onPipDismiss fast enough. - final boolean animate = mMenuState != MENU_STATE_CLOSE; - // Do not notify menu visibility when hiding the menu, the controller will do this when it - // handles the message - hideMenu(mController::onPipDismiss, false /* notifyMenuVisibility */, animate, - true /* resize */); + if (mMenuState != MENU_STATE_NONE) { + // Do not call hideMenu() directly. Instead, let the menu controller handle it just as + // any other dismissal that will update the touch state and fade out the PIP task + // and the menu view at the same time. + mController.onPipDismiss(); + } } private void showSettings() { @@ -514,4 +536,17 @@ public class PipMenuView extends FrameLayout { mMainExecutor.removeCallbacks(mHideMenuRunnable); mMainExecutor.executeDelayed(mHideMenuRunnable, recommendedTimeout); } + + private long getFadeOutDuration(@AnimationType int animationType) { + switch (animationType) { + case ANIM_TYPE_NONE: + return ANIMATION_NONE_DURATION_MS; + case ANIM_TYPE_HIDE: + return ANIMATION_HIDE_DURATION_MS; + case ANIM_TYPE_DISMISS: + return mDismissFadeOutDurationMs; + default: + throw new IllegalStateException("Invalid animation type " + animationType); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index 67467eda51d0..9401cd6a2954 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -22,6 +22,8 @@ import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; +import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_DISMISS; +import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -336,7 +338,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, + " callers=\n" + Debug.getCallers(5, " ")); } cancelPhysicsAnimation(); - mMenuController.hideMenu(false /* animate */, false /* resize */); + mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION); } @@ -349,7 +351,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, Log.d(TAG, "removePip: callers=\n" + Debug.getCallers(5, " ")); } cancelPhysicsAnimation(); - mMenuController.hideMenu(true /* animate*/, false /* resize */); + mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */); mPipTaskOrganizer.removePip(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java index c2ec1c55a59f..8726ee76d29a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java @@ -21,6 +21,7 @@ import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; +import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; import android.content.Context; import android.content.res.Resources; @@ -471,7 +472,7 @@ public class PipResizeGestureHandler { } if (mThresholdCrossed) { if (mPhonePipMenuController.isMenuVisible()) { - mPhonePipMenuController.hideMenu(false /* animate */, + mPhonePipMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); } final Rect currentPipBounds = mPipBoundsState.getBounds(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 6d96312ad962..0a0798ef24c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -25,6 +25,7 @@ import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; +import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; import android.annotation.NonNull; import android.annotation.SuppressLint; @@ -881,7 +882,7 @@ public class PipTouchHandler { && mPipBoundsState.getBounds().height() < mPipBoundsState.getMaxSize().y; if (mMenuController.isMenuVisible()) { - mMenuController.hideMenu(false /* animate */, false /* resize */); + mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); } if (toExpand) { mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 232de0b7c57f..fbf7def4093c 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -180,7 +180,7 @@ public final class MediaRouter2 { // SecurityException will be thrown if there's no permission. serviceBinder.enforceMediaContentControlPermission(); } catch (RemoteException e) { - Log.e(TAG, "Unable to check MEDIA_CONTENT_CONTROL permission."); + e.rethrowFromSystemServer(); } PackageManager pm = context.getPackageManager(); diff --git a/media/tests/MediaRouter/Android.bp b/media/tests/MediaRouter/Android.bp index d41bc02906ab..2da6c9884c0f 100644 --- a/media/tests/MediaRouter/Android.bp +++ b/media/tests/MediaRouter/Android.bp @@ -18,7 +18,9 @@ android_test { ], static_libs: [ + "androidx.test.core", "androidx.test.rules", + "compatibility-device-util-axt", "mockito-target-minus-junit4", "testng", "truth-prebuilt", diff --git a/media/tests/MediaRouter/AndroidManifest.xml b/media/tests/MediaRouter/AndroidManifest.xml index 02688d5d641a..018f148dd3d5 100644 --- a/media/tests/MediaRouter/AndroidManifest.xml +++ b/media/tests/MediaRouter/AndroidManifest.xml @@ -19,6 +19,7 @@ <application android:label="@string/app_name"> <uses-library android:name="android.test.runner" /> + <activity android:name="com.android.mediaroutertest.MediaRouter2ManagerTestActivity" /> <service android:name=".StubMediaRoute2ProviderService" android:exported="true"> <intent-filter> diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index eaa4f03ba81a..3a34e756a50a 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -16,7 +16,6 @@ package com.android.mediaroutertest; -import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; import static android.media.MediaRoute2Info.FEATURE_REMOTE_PLAYBACK; import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED; import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE; @@ -61,6 +60,8 @@ import androidx.test.filters.LargeTest; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.compatibility.common.util.PollingCheck; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -94,6 +95,7 @@ public class MediaRouter2ManagerTest { private MediaRouter2 mRouter2; private Executor mExecutor; private String mPackageName; + private StubMediaRoute2ProviderService mService; private final List<MediaRouter2Manager.Callback> mManagerCallbacks = new ArrayList<>(); private final List<RouteCallback> mRouteCallbacks = new ArrayList<>(); @@ -105,7 +107,6 @@ public class MediaRouter2ManagerTest { static { FEATURES_ALL.add(FEATURE_SAMPLE); FEATURES_ALL.add(FEATURE_SPECIAL); - FEATURES_ALL.add(FEATURE_LIVE_AUDIO); FEATURES_SPECIAL.add(FEATURE_SPECIAL); } @@ -115,26 +116,53 @@ public class MediaRouter2ManagerTest { mContext = InstrumentationRegistry.getTargetContext(); mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.MEDIA_CONTENT_CONTROL); + MediaRouter2ManagerTestActivity.startActivity(mContext); + mManager = MediaRouter2Manager.getInstance(mContext); + mManager.startScan(); mRouter2 = MediaRouter2.getInstance(mContext); + // If we need to support thread pool executors, change this to thread pool executor. mExecutor = Executors.newSingleThreadExecutor(); mPackageName = mContext.getPackageName(); + + // In order to make the system bind to the test service, + // set a non-empty discovery preference while app is in foreground. + List<String> features = new ArrayList<>(); + features.add("A test feature"); + RouteDiscoveryPreference preference = + new RouteDiscoveryPreference.Builder(features, false).build(); + mRouter2.registerRouteCallback(mExecutor, new RouteCallback() {}, preference); + + new PollingCheck(TIMEOUT_MS) { + @Override + protected boolean check() { + StubMediaRoute2ProviderService service = + StubMediaRoute2ProviderService.getInstance(); + if (service != null) { + mService = service; + return true; + } + return false; + } + }.run(); } @After public void tearDown() { + mManager.stopScan(); + // order matters (callbacks should be cleared at the last) releaseAllSessions(); // unregister callbacks clearCallbacks(); - StubMediaRoute2ProviderService instance = StubMediaRoute2ProviderService.getInstance(); - if (instance != null) { - instance.setProxy(null); - instance.setSpy(null); + if (mService != null) { + mService.setProxy(null); + mService.setSpy(null); } + MediaRouter2ManagerTestActivity.finishActivity(); mUiAutomation.dropShellPermissionIdentity(); } @@ -179,13 +207,10 @@ public class MediaRouter2ManagerTest { MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); assertNotNull(routeToRemove); - StubMediaRoute2ProviderService sInstance = - StubMediaRoute2ProviderService.getInstance(); - assertNotNull(sInstance); - sInstance.removeRoute(ROUTE_ID2); + mService.removeRoute(ROUTE_ID2); assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - sInstance.addRoute(routeToRemove); + mService.addRoute(routeToRemove); assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } @@ -218,10 +243,7 @@ public class MediaRouter2ManagerTest { MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); assertNotNull(routeToRemove); - StubMediaRoute2ProviderService sInstance = - StubMediaRoute2ProviderService.getInstance(); - assertNotNull(sInstance); - sInstance.removeRoute(ROUTE_ID2); + mService.removeRoute(ROUTE_ID2); // Wait until the route is removed. assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); @@ -230,7 +252,7 @@ public class MediaRouter2ManagerTest { assertNull(newRoutes.get(ROUTE_ID2)); // Revert the removal. - sInstance.addRoute(routeToRemove); + mService.addRoute(routeToRemove); assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); mRouter2.unregisterRouteCallback(routeCallback); } @@ -445,9 +467,7 @@ public class MediaRouter2ManagerTest { CountDownLatch serviceOnReleaseSessionLatch = new CountDownLatch(1); List<RoutingSessionInfo> sessions = new ArrayList<>(); - StubMediaRoute2ProviderService instance = StubMediaRoute2ProviderService.getInstance(); - assertNotNull(instance); - instance.setSpy(new StubMediaRoute2ProviderService.Spy() { + mService.setSpy(new StubMediaRoute2ProviderService.Spy() { @Override public void onReleaseSession(long requestId, String sessionId) { serviceOnReleaseSessionLatch.countDown(); @@ -652,12 +672,9 @@ public class MediaRouter2ManagerTest { Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); MediaRoute2Info volRoute = routes.get(ROUTE_ID_VARIABLE_VOLUME); - StubMediaRoute2ProviderService instance = StubMediaRoute2ProviderService.getInstance(); - assertNotNull(instance); - final List<Long> requestIds = new ArrayList<>(); final CountDownLatch onSetRouteVolumeLatch = new CountDownLatch(1); - instance.setProxy(new StubMediaRoute2ProviderService.Proxy() { + mService.setProxy(new StubMediaRoute2ProviderService.Proxy() { @Override public void onSetRouteVolume(String routeId, int volume, long requestId) { requestIds.add(requestId); @@ -687,16 +704,16 @@ public class MediaRouter2ManagerTest { }); final long invalidRequestId = REQUEST_ID_NONE; - instance.notifyRequestFailed(invalidRequestId, failureReason); + mService.notifyRequestFailed(invalidRequestId, failureReason); assertFalse(onRequestFailedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)); final long validRequestId = requestIds.get(0); - instance.notifyRequestFailed(validRequestId, failureReason); + mService.notifyRequestFailed(validRequestId, failureReason); assertTrue(onRequestFailedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); // Test calling notifyRequestFailed() multiple times with the same valid requestId. // onRequestFailed() shouldn't be called since the requestId has been already handled. - instance.notifyRequestFailed(validRequestId, failureReason); + mService.notifyRequestFailed(validRequestId, failureReason); assertFalse(onRequestFailedSecondCallLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } @@ -813,7 +830,8 @@ public class MediaRouter2ManagerTest { @Override public void onRoutesAdded(List<MediaRoute2Info> routes) { for (MediaRoute2Info route : routes) { - if (!route.isSystemRoute()) { + if (!route.isSystemRoute() + && hasMatchingFeature(route.getFeatures(), routeFeatures)) { addedLatch.countDown(); break; } @@ -834,10 +852,10 @@ public class MediaRouter2ManagerTest { mRouter2.registerRouteCallback(mExecutor, routeCallback, new RouteDiscoveryPreference.Builder(routeFeatures, true).build()); try { - if (mManager.getAllRoutes().isEmpty()) { + featuresLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS); + if (mManager.getAvailableRoutes(mPackageName).isEmpty()) { addedLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS); } - featuresLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS); return createRouteMap(mManager.getAvailableRoutes(mPackageName)); } finally { mRouter2.unregisterRouteCallback(routeCallback); @@ -845,6 +863,15 @@ public class MediaRouter2ManagerTest { } } + boolean hasMatchingFeature(List<String> features1, List<String> features2) { + for (String feature : features1) { + if (features2.contains(feature)) { + return true; + } + } + return false; + } + void awaitOnRouteChangedManager(Runnable task, String routeId, Predicate<MediaRoute2Info> predicate) throws Exception { CountDownLatch latch = new CountDownLatch(1); diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTestActivity.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTestActivity.java new file mode 100644 index 000000000000..ac2a8bbd4e50 --- /dev/null +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTestActivity.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2021 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.mediaroutertest; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.WindowManager; + +import androidx.test.core.app.ActivityScenario; + +public class MediaRouter2ManagerTestActivity extends Activity { + + private static ActivityScenario<MediaRouter2ManagerTestActivity> sActivityScenario; + + public static ActivityScenario<MediaRouter2ManagerTestActivity> startActivity(Context context) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(context, MediaRouter2ManagerTestActivity.class); + sActivityScenario = ActivityScenario.launch(intent); + return sActivityScenario; + } + + public static void finishActivity() { + if (sActivityScenario != null) { + // TODO: Sometimes calling this takes about 5 seconds. Need to figure out why. + sActivityScenario.close(); + sActivityScenario = null; + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTurnScreenOn(true); + setShowWhenLocked(true); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } +} diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index 7540a143c2f3..a8c2ea544d38 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -438,27 +438,44 @@ void ASurfaceTransaction_setGeometry(ASurfaceTransaction* aSurfaceTransaction, const ARect& destination, int32_t transform) { CHECK_NOT_NULL(aSurfaceTransaction); CHECK_NOT_NULL(aSurfaceControl); + CHECK_VALID_RECT(source); CHECK_VALID_RECT(destination); + sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl); + Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction); + Rect sourceRect = static_cast<const Rect&>(source); + Rect destRect = static_cast<const Rect&>(destination); // Adjust the source so its top and left are not negative sourceRect.left = std::max(sourceRect.left, 0); sourceRect.top = std::max(sourceRect.top, 0); - LOG_ALWAYS_FATAL_IF(sourceRect.isEmpty(), "invalid arg passed as source argument"); - - sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl); - Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction); + if (!sourceRect.isValid()) { + sourceRect.makeInvalid(); + } transaction->setBufferCrop(surfaceControl, sourceRect); - float dsdx = (destination.right - destination.left) / - static_cast<float>(sourceRect.right - sourceRect.left); - float dsdy = (destination.bottom - destination.top) / - static_cast<float>(sourceRect.bottom - sourceRect.top); + int destW = destRect.width(); + int destH = destRect.height(); + if (destRect.left < 0) { + destRect.left = 0; + destRect.right = destW; + } + if (destRect.top < 0) { + destRect.top = 0; + destRect.bottom = destH; + } + + if (!sourceRect.isEmpty()) { + float sx = destW / static_cast<float>(sourceRect.width()); + float sy = destH / static_cast<float>(sourceRect.height()); + transaction->setPosition(surfaceControl, destRect.left - (sourceRect.left * sx), + destRect.top - (sourceRect.top * sy)); + transaction->setMatrix(surfaceControl, sx, 0, 0, sy); + } else { + transaction->setPosition(surfaceControl, destRect.left, destRect.top); + } - transaction->setPosition(surfaceControl, destination.left - (sourceRect.left * dsdx), - destination.top - (sourceRect.top * dsdy)); - transaction->setMatrix(surfaceControl, dsdx, 0, 0, dsdy); transaction->setTransform(surfaceControl, transform); bool transformToInverseDisplay = (NATIVE_WINDOW_TRANSFORM_INVERSE_DISPLAY & transform) == NATIVE_WINDOW_TRANSFORM_INVERSE_DISPLAY; diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 6d6bc07c01b5..20273d05f059 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -8,6 +8,7 @@ import android.app.PendingIntent import android.content.Context import android.graphics.Matrix import android.graphics.Rect +import android.os.Looper import android.os.RemoteException import android.util.MathUtils import android.view.IRemoteAnimationFinishedCallback @@ -73,16 +74,20 @@ class ActivityLaunchAnimator(context: Context) { * in [Controller.onLaunchAnimationProgress]. No animation will start if there is no window * opening. * - * If [controller] is null, then the intent will be started and no animation will run. + * If [controller] is null or [animate] is false, then the intent will be started and no + * animation will run. * * This method will throw any exception thrown by [intentStarter]. */ + @JvmOverloads inline fun startIntentWithAnimation( controller: Controller?, + animate: Boolean = true, intentStarter: (RemoteAnimationAdapter?) -> Int ) { - if (controller == null) { + if (controller == null || !animate) { intentStarter(null) + controller?.callOnIntentStartedOnMainThread(willAnimate = false) return } @@ -95,7 +100,7 @@ class ActivityLaunchAnimator(context: Context) { val launchResult = intentStarter(animationAdapter) val willAnimate = launchResult == ActivityManager.START_TASK_TO_FRONT || launchResult == ActivityManager.START_SUCCESS - runner.context.mainExecutor.execute { controller.onIntentStarted(willAnimate) } + controller.callOnIntentStartedOnMainThread(willAnimate) // If we expect an animation, post a timeout to cancel it in case the remote animation is // never started. @@ -104,17 +109,30 @@ class ActivityLaunchAnimator(context: Context) { } } + @PublishedApi + internal fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) { + if (Looper.myLooper() != Looper.getMainLooper()) { + this.getRootView().context.mainExecutor.execute { + this.onIntentStarted(willAnimate) + } + } else { + this.onIntentStarted(willAnimate) + } + } + /** * Same as [startIntentWithAnimation] but allows [intentStarter] to throw a * [PendingIntent.CanceledException] which must then be handled by the caller. This is useful * for Java caller starting a [PendingIntent]. */ @Throws(PendingIntent.CanceledException::class) + @JvmOverloads fun startPendingIntentWithAnimation( controller: Controller?, + animate: Boolean = true, intentStarter: PendingIntentStarter ) { - startIntentWithAnimation(controller) { intentStarter.startPendingIntent(it) } + startIntentWithAnimation(controller, animate) { intentStarter.startPendingIntent(it) } } /** Create a new animation [Runner] controlled by [controller]. */ diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml index bad582669079..676e49298cef 100644 --- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml @@ -22,14 +22,16 @@ android:layout_height="match_parent" android:layout_width="wrap_content" android:layout_gravity="center_vertical|end" - android:focusable="true" - android:minWidth="48dp" > + android:focusable="true" > <LinearLayout android:id="@+id/icons_container" android:layout_height="@dimen/ongoing_appops_chip_height" android:layout_width="wrap_content" - android:gravity="center_vertical" + android:paddingStart="10dp" + android:paddingEnd="10dp" + android:gravity="center" android:layout_gravity="center" + android:minWidth="56dp" /> </com.android.systemui.privacy.OngoingPrivacyChip>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_tile_emoji_background_large.xml b/packages/SystemUI/res/layout/people_tile_emoji_background_large.xml new file mode 100644 index 000000000000..61d3ea5df6df --- /dev/null +++ b/packages/SystemUI/res/layout/people_tile_emoji_background_large.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/emojis" + android:theme="@android:style/Theme.DeviceDefault.DayNight" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="10dp"> + <TextView + android:id="@+id/emoji1" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:paddingStart="40dp" + android:maxLines="1" + android:alpha="0.2"/> + <TextView + android:id="@+id/emoji2" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_alignParentStart="true" + android:paddingTop="5dp" + android:paddingStart="27dp" + android:maxLines="1" + android:alpha="0.2"/> + <TextView + android:id="@+id/emoji3" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:paddingEnd="25dp" + android:maxLines="1" + android:alpha="0.2"/> +</RelativeLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_tile_emoji_background_medium.xml b/packages/SystemUI/res/layout/people_tile_emoji_background_medium.xml new file mode 100644 index 000000000000..a5f300fa54b1 --- /dev/null +++ b/packages/SystemUI/res/layout/people_tile_emoji_background_medium.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/emojis" + android:theme="@android:style/Theme.DeviceDefault.DayNight" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:gravity="center_vertical"> + <TextView + android:id="@+id/emoji1" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="27sp" + android:paddingTop="-2dp" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="2" + android:gravity="end" + android:maxLines="1" + android:alpha="0.2"/> + <TextView + android:id="@+id/emoji2" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="27sp" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:gravity="end" + android:paddingTop="20dp" + android:maxLines="1" + android:alpha="0.2"/> + <TextView + android:id="@+id/emoji3" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="27sp" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_gravity="top" + android:paddingTop="10dp" + android:maxLines="1" + android:alpha="0.2"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_tile_large_with_content.xml b/packages/SystemUI/res/layout/people_tile_large_with_content.xml index af2c5de3b52f..6f8de3b68589 100644 --- a/packages/SystemUI/res/layout/people_tile_large_with_content.xml +++ b/packages/SystemUI/res/layout/people_tile_large_with_content.xml @@ -67,68 +67,79 @@ android:visibility="gone" /> </RelativeLayout> - - <TextView - android:layout_gravity="center" - android:id="@+id/name" + <RelativeLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingBottom="12dp" - android:gravity="start" - android:singleLine="true" - android:ellipsize="end" - android:text="@string/empty_user_name" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorPrimary" - android:textSize="14sp" /> + android:layout_height="match_parent"> + <include layout="@layout/people_tile_punctuation_background_large" /> + <include layout="@layout/people_tile_emoji_background_large" /> - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingBottom="4dp" - android:gravity="center_vertical" - android:orientation="horizontal"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <TextView + android:layout_gravity="center" + android:id="@+id/name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="12dp" + android:gravity="start" + android:singleLine="true" + android:ellipsize="end" + android:text="@string/empty_user_name" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textColor="?android:attr/textColorPrimary" + android:textSize="14sp" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="4dp" + android:gravity="center_vertical" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/predefined_icon" + android:tint="?android:attr/colorAccent" + android:gravity="start|center_vertical" + android:paddingEnd="6dp" + android:layout_width="24dp" + android:layout_height="18dp" /> + + <TextView + android:layout_gravity="center" + android:id="@+id/subtext" + android:gravity="center_vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:singleLine="true" + android:text="@string/empty_user_name" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textColor="?android:attr/textColorSecondary" + android:textSize="12sp" /> + </LinearLayout> <ImageView - android:id="@+id/predefined_icon" - android:tint="?android:attr/colorAccent" - android:gravity="start|center_vertical" - android:paddingEnd="6dp" - android:layout_width="24dp" - android:layout_height="18dp" /> + android:id="@+id/image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/people_space_content_background" + android:gravity="center" + android:scaleType="centerCrop" /> <TextView android:layout_gravity="center" - android:id="@+id/subtext" - android:gravity="center_vertical" + android:id="@+id/text_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" - android:singleLine="true" - android:text="@string/empty_user_name" + android:maxLines="2" + android:singleLine="false" + android:text="@string/empty_status" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorSecondary" + android:textColor="?android:attr/textColorPrimary" android:textSize="12sp" /> - </LinearLayout> - - <ImageView - android:id="@+id/image" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@drawable/people_space_content_background" - android:gravity="center" - android:scaleType="centerCrop" /> - - <TextView - android:layout_gravity="center" - android:id="@+id/text_content" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:ellipsize="end" - android:maxLines="2" - android:singleLine="false" - android:text="@string/empty_status" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorPrimary" - android:textSize="12sp" /> + </LinearLayout> + </RelativeLayout> </LinearLayout> diff --git a/packages/SystemUI/res/layout/people_tile_medium_with_content.xml b/packages/SystemUI/res/layout/people_tile_medium_with_content.xml index 70706600e6da..a8c15abc8459 100644 --- a/packages/SystemUI/res/layout/people_tile_medium_with_content.xml +++ b/packages/SystemUI/res/layout/people_tile_medium_with_content.xml @@ -21,121 +21,127 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> - <LinearLayout + <RelativeLayout android:background="@drawable/people_space_tile_view_card" - android:id="@+id/item" - android:orientation="vertical" - android:layout_gravity="center" - android:padding="8dp" android:layout_width="match_parent" android:layout_height="match_parent"> - + <include layout="@layout/people_tile_punctuation_background_medium" /> + <include layout="@layout/people_tile_emoji_background_medium" /> <LinearLayout - android:orientation="horizontal" - android:gravity="top" - android:layout_weight="1" + android:id="@+id/item" + android:orientation="vertical" + android:layout_gravity="center" + android:padding="8dp" android:layout_width="match_parent" - android:layout_height="0dp"> + android:layout_height="match_parent"> + + <LinearLayout + android:orientation="horizontal" + android:gravity="top" + android:layout_weight="1" + android:layout_width="match_parent" + android:layout_height="0dp"> - <ImageView - android:gravity="start" - android:id="@+id/person_icon" - android:layout_marginStart="-2dp" - android:layout_marginTop="-2dp" - android:layout_width="52dp" - android:layout_height="52dp" /> + <ImageView + android:gravity="start" + android:id="@+id/person_icon" + android:layout_marginStart="-2dp" + android:layout_marginTop="-2dp" + android:layout_width="52dp" + android:layout_height="52dp" /> - <ImageView - android:id="@+id/availability" - android:layout_marginStart="-2dp" - android:layout_width="10dp" - android:layout_height="10dp" - android:background="@drawable/circle_green_10dp" /> + <ImageView + android:id="@+id/availability" + android:layout_marginStart="-2dp" + android:layout_width="10dp" + android:layout_height="10dp" + android:background="@drawable/circle_green_10dp" /> + + <LinearLayout + android:orientation="vertical" + android:gravity="top|start" + android:paddingStart="12dp" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/subtext" + android:text="@string/empty_user_name" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textColor="?android:attr/textColorSecondary" + android:textSize="12sp" + android:paddingBottom="4dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:ellipsize="end" /> + + <ImageView + android:id="@+id/image" + android:gravity="center" + android:background="@drawable/people_space_content_background" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" /> + + <TextView + android:id="@+id/text_content" + android:text="@string/empty_status" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textColor="?android:attr/textColorPrimary" + android:textSize="12sp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:maxLines="2" + android:singleLine="false" + android:ellipsize="end" /> + </LinearLayout> + </LinearLayout> <LinearLayout - android:orientation="vertical" - android:gravity="top|start" - android:paddingStart="12dp" + android:gravity="bottom" + android:layout_gravity="center_vertical" + android:orientation="horizontal" + android:paddingTop="2dp" android:layout_width="match_parent" - android:layout_height="wrap_content"> - + android:layout_height="wrap_content" + android:clipToOutline="true"> <TextView - android:id="@+id/subtext" + android:id="@+id/name" + android:gravity="center_vertical" + android:layout_weight="1" android:text="@string/empty_user_name" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorSecondary" - android:textSize="12sp" - android:paddingBottom="4dp" - android:layout_width="match_parent" - android:layout_height="wrap_content" + android:textColor="?android:attr/textColorPrimary" + android:textSize="14sp" android:singleLine="true" - android:ellipsize="end" /> - - <ImageView - android:id="@+id/image" - android:gravity="center" - android:background="@drawable/people_space_content_background" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scaleType="centerCrop" /> - + android:ellipsize="end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> <TextView - android:id="@+id/text_content" - android:text="@string/empty_status" + android:id="@+id/messages_count" + android:gravity="end" + android:paddingStart="8dp" + android:paddingEnd="8dp" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" android:textColor="?android:attr/textColorPrimary" - android:textSize="12sp" - android:layout_width="match_parent" + android:background="@drawable/people_space_messages_count_background" + android:textSize="14sp" + android:maxLines="1" + android:ellipsize="end" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:maxLines="2" - android:singleLine="false" - android:ellipsize="end" /> + android:visibility="gone" + /> + <ImageView + android:id="@+id/predefined_icon" + android:tint="?android:attr/colorAccent" + android:gravity="end|center_vertical" + android:paddingStart="6dp" + android:layout_width="24dp" + android:layout_height="18dp" /> </LinearLayout> </LinearLayout> - - <LinearLayout - android:gravity="bottom" - android:layout_gravity="center_vertical" - android:orientation="horizontal" - android:paddingTop="2dp" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:clipToOutline="true"> - <TextView - android:id="@+id/name" - android:gravity="center_vertical" - android:layout_weight="1" - android:text="@string/empty_user_name" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorPrimary" - android:textSize="14sp" - android:singleLine="true" - android:ellipsize="end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - <TextView - android:id="@+id/messages_count" - android:gravity="end" - android:paddingStart="8dp" - android:paddingEnd="8dp" - android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" - android:textColor="?android:attr/textColorPrimary" - android:background="@drawable/people_space_messages_count_background" - android:textSize="14sp" - android:maxLines="1" - android:ellipsize="end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:visibility="gone" - /> - <ImageView - android:id="@+id/predefined_icon" - android:tint="?android:attr/colorAccent" - android:gravity="end|center_vertical" - android:paddingStart="6dp" - android:layout_width="24dp" - android:layout_height="18dp" /> - </LinearLayout> - </LinearLayout> + </RelativeLayout> </LinearLayout> diff --git a/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml b/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml new file mode 100644 index 000000000000..2ffe59a2d032 --- /dev/null +++ b/packages/SystemUI/res/layout/people_tile_punctuation_background_large.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:theme="@android:style/Theme.DeviceDefault.DayNight" + android:id="@+id/punctuations" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="10dp"> + <TextView + android:id="@+id/punctuation1" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:textStyle="bold" + android:paddingTop="-2dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:maxLines="1" + android:rotation="5"/> + <TextView + android:id="@+id/punctuation2" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:textStyle="bold" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_alignParentStart="true" + android:paddingTop="5dp" + android:paddingStart="27dp" + android:includeFontPadding="false" + android:maxLines="1" + android:rotation="350"/> + <TextView + android:id="@+id/punctuation3" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:textStyle="bold" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:paddingEnd="25dp" + android:includeFontPadding="false" + android:maxLines="1" + android:rotation="350"/> + <TextView + android:id="@+id/punctuation4" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:textStyle="bold" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentStart="true" + android:paddingStart="15dp" + android:includeFontPadding="false" + android:maxLines="1" + android:rotation="10"/> + <TextView + android:id="@+id/punctuation5" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:textStyle="bold" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" + android:paddingTop="15dp" + android:includeFontPadding="false" + android:maxLines="1" + android:rotation="5"/> + <TextView + android:id="@+id/punctuation6" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="34sp" + android:textStyle="bold" + android:paddingStart="20dp" + android:paddingTop="30dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_centerHorizontal="true" + android:maxLines="1" + android:rotation="350"/> +</RelativeLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml b/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml new file mode 100644 index 000000000000..75cdde0e97e4 --- /dev/null +++ b/packages/SystemUI/res/layout/people_tile_punctuation_background_medium.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:theme="@android:style/Theme.DeviceDefault.DayNight" + android:id="@+id/punctuations" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal"> + <TextView + android:id="@+id/punctuation1" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="27sp" + android:textStyle="bold" + android:paddingTop="-2dp" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="4" + android:gravity="center_vertical|end" + android:maxLines="1" + android:rotation="5"/> + <TextView + android:id="@+id/punctuation2" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="27sp" + android:textStyle="bold" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:layout_gravity="top" + android:gravity="top|end" + android:includeFontPadding="false" + android:paddingTop="25dp" + android:maxLines="1" + android:rotation="350"/> + <TextView + android:id="@+id/punctuation3" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="27sp" + android:textStyle="bold" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center|end" + android:includeFontPadding="false" + android:paddingTop="10dp" + android:maxLines="1" + android:rotation="350"/> + <TextView + android:id="@+id/punctuation4" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="27sp" + android:textStyle="bold" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:paddingTop="10dp" + android:gravity="start|top" + android:includeFontPadding="false" + android:maxLines="1" + android:rotation="10"/> + <TextView + android:id="@+id/punctuation5" + android:textColor="?androidprv:attr/colorSurfaceVariant" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" + android:textSize="27sp" + android:textStyle="bold" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:paddingTop="15dp" + android:gravity="start|center_vertical" + android:includeFontPadding="false" + android:maxLines="1" + android:rotation="350"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index bf13c2178d45..ee25a1059d63 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -280,8 +280,7 @@ <color name="screenrecord_status_color">#E94235</color> - <!-- TODO(b/178093014) Colors for privacy dialog. These should be changed to the new palette --> - <color name="privacy_circle">#1E8E3E</color> <!-- g600 --> + <color name="privacy_circle">#5BB974</color> <!-- g400 --> <!-- Accessibility floating menu --> <color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 6c66d0748f76..13c285f12393 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -181,6 +181,9 @@ <!-- Radius for notifications corners without adjacent notifications --> <dimen name="notification_corner_radius">28dp</dimen> + <!-- Distance over which notification corner animations run, near the shelf while scrolling. --> + <dimen name="notification_corner_animation_distance">48dp</dimen> + <!-- Radius for notifications corners with adjacent notifications --> <dimen name="notification_corner_radius_small">4dp</dimen> @@ -1237,7 +1240,7 @@ <!-- Icon size of Ongoing App Ops chip --> <dimen name="ongoing_appops_chip_icon_size">16dp</dimen> <!-- Radius of Ongoing App Ops chip corners --> - <dimen name="ongoing_appops_chip_bg_corner_radius">16dp</dimen> + <dimen name="ongoing_appops_chip_bg_corner_radius">28dp</dimen> <dimen name="ongoing_appops_dialog_side_margins">@dimen/notification_shade_content_margin_horizontal</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cdbfa13da127..673a03ddac4e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2729,6 +2729,8 @@ <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half">Move to edge and hide</string> <!-- Action in accessibility menu to move the accessibility floating button out the edge and show. [CHAR LIMIT=36]--> <string name="accessibility_floating_button_action_move_out_edge_and_show">Move out edge and show</string> + <!-- Action in accessibility menu to toggle on/off the accessibility feature. [CHAR LIMIT=30]--> + <string name="accessibility_floating_button_action_double_tap_to_toggle">toggle</string> <!-- Device Controls strings --> <!-- Device Controls empty state, title [CHAR LIMIT=30] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 2e45acc2cbfa..ac9ced93f2df 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -787,15 +787,16 @@ <style name="TextAppearance.ControlSetup"> <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> <item name="android:textColor">@color/control_primary_text</item> - <item name="android:singleLine">true</item> </style> <style name="TextAppearance.ControlSetup.Title"> <item name="android:textSize">@dimen/controls_setup_title</item> + <item name="android:singleLine">true</item> </style> <style name="TextAppearance.ControlSetup.Subtitle"> <item name="android:textSize">@dimen/controls_setup_subtitle</item> + <item name="android:maxLines">2</item> </style> <!-- The attributes used for title (textAppearanceLarge) and message (textAppearanceMedium) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java index 7b4ce61d2cfe..3b3bad3b255f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenu.java @@ -70,9 +70,16 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { } }; + private final ContentObserver mEnabledA11yServicesContentObserver = + new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + mMenuView.onEnabledFeaturesChanged(); + } + }; + public AccessibilityFloatingMenu(Context context) { - mContext = context; - mMenuView = new AccessibilityFloatingMenuView(context); + this(context, new AccessibilityFloatingMenuView(context)); } @VisibleForTesting @@ -153,11 +160,17 @@ public class AccessibilityFloatingMenu implements IAccessibilityFloatingMenu { Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY), /* notifyForDescendants */ false, mFadeOutContentObserver, UserHandle.USER_CURRENT); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES), + /* notifyForDescendants */ false, + mEnabledA11yServicesContentObserver, UserHandle.USER_CURRENT); } private void unregisterContentObservers() { mContext.getContentResolver().unregisterContentObserver(mContentObserver); mContext.getContentResolver().unregisterContentObserver(mSizeContentObserver); mContext.getContentResolver().unregisterContentObserver(mFadeOutContentObserver); + mContext.getContentResolver().unregisterContentObserver( + mEnabledA11yServicesContentObserver); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java index 5502a20ce398..934e20dfbd05 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuView.java @@ -24,6 +24,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.PixelFormat; @@ -105,6 +106,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout private float mRadius; private float mPercentageY = LOCATION_Y_PERCENTAGE; private float mSquareScaledTouchSlop; + private final Configuration mLastConfiguration; private final RecyclerView mListView; private final AccessibilityTargetAdapter mAdapter; private float mFadeOutValue; @@ -202,6 +204,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout } }); + mLastConfiguration = new Configuration(getResources().getConfiguration()); + updateDimensions(); initListView(); updateStrokeWith(getResources().getConfiguration().uiMode, mAlignment); @@ -368,7 +372,7 @@ public class AccessibilityFloatingMenuView extends FrameLayout mTargets.clear(); mTargets.addAll(newTargets); - mAdapter.notifyDataSetChanged(); + onEnabledFeaturesChanged(); updateRadiusWith(mSizeType, mRadiusType, mTargets.size()); updateScrollModeWith(hasExceededMaxLayoutHeight()); @@ -416,6 +420,10 @@ public class AccessibilityFloatingMenuView extends FrameLayout setAlpha(mIsFadeEffectEnabled ? mFadeOutValue : /* completely opaque */ 1.0f); } + void onEnabledFeaturesChanged() { + mAdapter.notifyDataSetChanged(); + } + @VisibleForTesting void fadeIn() { if (!mIsFadeEffectEnabled) { @@ -601,13 +609,17 @@ public class AccessibilityFloatingMenuView extends FrameLayout params.gravity = Gravity.START | Gravity.TOP; params.x = getMaxWindowX(); params.y = (int) (getMaxWindowY() * mPercentageY); - + updateAccessibilityTitle(params); return params; } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + final int diff = newConfig.diff(mLastConfiguration); + if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) { + updateAccessibilityTitle(mCurrentLayoutParams); + } updateDimensions(); updateListView(); @@ -616,6 +628,8 @@ public class AccessibilityFloatingMenuView extends FrameLayout updateStrokeWith(newConfig.uiMode, mAlignment); updateLocationWith(mAlignment, mPercentageY); updateScrollModeWith(hasExceededMaxLayoutHeight()); + + mLastConfiguration.setTo(newConfig); } @VisibleForTesting @@ -724,6 +738,11 @@ public class AccessibilityFloatingMenuView extends FrameLayout setInset(insetLeft, insetRight); } + private void updateAccessibilityTitle(WindowManager.LayoutParams params) { + params.accessibilityTitle = getResources().getString( + com.android.internal.R.string.accessibility_select_shortcut_menu_title); + } + private void setInset(int left, int right) { final LayerDrawable layerDrawable = getMenuLayerDrawable(); if (layerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM) == left diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java index fd0c4ef0a5be..76106e7c2cf1 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapter.java @@ -24,9 +24,12 @@ import android.view.ViewGroup; import androidx.annotation.IntDef; import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.Adapter; +import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.R; import com.android.systemui.accessibility.floatingmenu.AccessibilityTargetAdapter.ViewHolder; @@ -78,9 +81,20 @@ public class AccessibilityTargetAdapter extends Adapter<ViewHolder> { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - holder.mIconView.setBackground(mTargets.get(position).getIcon()); + final AccessibilityTarget target = mTargets.get(position); + holder.mIconView.setBackground(target.getIcon()); holder.updateIconWidthHeight(mIconWidthHeight); - holder.itemView.setOnClickListener((v) -> mTargets.get(position).onSelected()); + holder.itemView.setOnClickListener((v) -> target.onSelected()); + holder.itemView.setStateDescription(target.getStateDescription()); + holder.itemView.setContentDescription(target.getLabel()); + + final String clickHint = target.getFragmentType() == AccessibilityFragmentType.TOGGLE + ? holder.itemView.getResources().getString( + R.string.accessibility_floating_button_action_double_tap_to_toggle) + : null; + ViewCompat.replaceAccessibilityAction(holder.itemView, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + clickHint, /* command= */ null); } @ItemType diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 666afed41c35..f1431f5cd40b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -51,6 +51,7 @@ import com.android.internal.policy.IKeyguardExitCallback; import com.android.internal.policy.IKeyguardService; import com.android.internal.policy.IKeyguardStateCallback; import com.android.systemui.SystemUIApplication; +import com.android.wm.shell.transition.Transitions; import javax.inject.Inject; @@ -62,16 +63,29 @@ public class KeyguardService extends Service { * Run Keyguard animation as remote animation in System UI instead of local animation in * the server process. * + * 0: Runs all keyguard animation as local animation + * 1: Only runs keyguard going away animation as remote animation + * 2: Runs all keyguard animation as remote animation + * * Note: Must be consistent with WindowManagerService. */ private static final String ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY = "persist.wm.enable_remote_keyguard_animation"; + private static final int sEnableRemoteKeyguardAnimation = + SystemProperties.getInt(ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY, 1); + /** * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY */ - static boolean sEnableRemoteKeyguardAnimation = - SystemProperties.getBoolean(ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY, false); + public static boolean sEnableRemoteKeyguardGoingAwayAnimation = + !Transitions.ENABLE_SHELL_TRANSITIONS && sEnableRemoteKeyguardAnimation >= 1; + + /** + * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY + */ + public static boolean sEnableRemoteKeyguardOccludeAnimation = + !Transitions.ENABLE_SHELL_TRANSITIONS && sEnableRemoteKeyguardAnimation >= 2; private final KeyguardViewMediator mKeyguardViewMediator; private final KeyguardLifecyclesDispatcher mKeyguardLifecyclesDispatcher; @@ -83,20 +97,22 @@ public class KeyguardService extends Service { mKeyguardViewMediator = keyguardViewMediator; mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher; - if (sEnableRemoteKeyguardAnimation) { - RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); + RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); + if (sEnableRemoteKeyguardGoingAwayAnimation) { final RemoteAnimationAdapter exitAnimationAdapter = new RemoteAnimationAdapter(mExitAnimationRunner, 0, 0); definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, exitAnimationAdapter); definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER, exitAnimationAdapter); + } + if (sEnableRemoteKeyguardOccludeAnimation) { final RemoteAnimationAdapter occludeAnimationAdapter = new RemoteAnimationAdapter(mOccludeAnimationRunner, 0, 0); definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_OCCLUDE, occludeAnimationAdapter); definition.addRemoteAnimation(TRANSIT_OLD_KEYGUARD_UNOCCLUDE, occludeAnimationAdapter); - ActivityTaskManager.getInstance().registerRemoteAnimationsForDisplay( - DEFAULT_DISPLAY, definition); } + ActivityTaskManager.getInstance().registerRemoteAnimationsForDisplay( + DEFAULT_DISPLAY, definition); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 411c328cd310..85ee0dca8805 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -280,7 +280,7 @@ class KeyguardUnlockAnimationController @Inject constructor( } override fun onKeyguardDismissAmountChanged() { - if (!KeyguardService.sEnableRemoteKeyguardAnimation) { + if (!KeyguardService.sEnableRemoteKeyguardGoingAwayAnimation) { return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 48f9a58d7d1a..b7da7addf027 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -2100,7 +2100,7 @@ public class KeyguardViewMediator extends SystemUI implements Dumpable, playSounds(false); } - if (KeyguardService.sEnableRemoteKeyguardAnimation) { + if (KeyguardService.sEnableRemoteKeyguardGoingAwayAnimation) { mSurfaceBehindRemoteAnimationFinishedCallback = finishedCallback; mSurfaceBehindRemoteAnimationRunning = true; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 56375adb3d4e..4e41d75e3f43 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -101,6 +101,7 @@ import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener; +import android.view.inputmethod.InputMethodManager; import androidx.annotation.VisibleForTesting; @@ -1175,6 +1176,9 @@ public class NavigationBar implements View.OnAttachStateChangeListener, accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick); updateAccessibilityServicesState(mAccessibilityManager); + ButtonDispatcher imeSwitcherButton = mNavigationBarView.getImeSwitchButton(); + imeSwitcherButton.setOnClickListener(this::onImeSwitcherClick); + updateScreenPinningGestures(); } @@ -1274,6 +1278,11 @@ public class NavigationBar implements View.OnAttachStateChangeListener, mCommandQueue.toggleRecentApps(); } + private void onImeSwitcherClick(View v) { + mContext.getSystemService(InputMethodManager.class).showInputMethodPickerFromSystem( + true /* showAuxiliarySubtypes */, mDisplayId); + }; + private boolean onLongPressBackHome(View v) { return onLongPressNavigationButtons(v, R.id.back, R.id.home); } @@ -1282,7 +1291,6 @@ public class NavigationBar implements View.OnAttachStateChangeListener, return onLongPressNavigationButtons(v, R.id.back, R.id.recent_apps); } - /** * This handles long-press of both back and recents/home. Back is the common button with * combination of recents if it is visible or home if recents is invisible. diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java index 3544f60601a8..66cfae4315d8 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java @@ -217,6 +217,9 @@ public class NavigationBarController implements Callbacks, @Override public void onNavigationModeChanged(int mode) { + if (mNavMode == mode) { + return; + } final int oldMode = mNavMode; mNavMode = mode; mHandler.post(() -> { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java index 7342f91a8108..4d9175b8db68 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarInflaterView.java @@ -158,7 +158,6 @@ public class NavigationBarInflaterView extends FrameLayout } public void onLikelyDefaultLayoutChange() { - // Reevaluate new layout final String newValue = getDefaultLayout(); if (!Objects.equals(mCurrentLayout, newValue)) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java index 0ed4d861c712..bdd273515619 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java @@ -166,6 +166,7 @@ public class NavigationBarView extends FrameLayout implements private NavigationBarInflaterView mNavigationInflaterView; private RecentsOnboarding mRecentsOnboarding; private NotificationPanelViewController mPanelView; + private RotationContextButton mRotationContextButton; private FloatingRotationButton mFloatingRotationButton; private RotationButtonController mRotationButtonController; private NavigationBarOverlayController mNavBarOverlayController; @@ -233,14 +234,6 @@ public class NavigationBarView extends FrameLayout implements } } - private final OnClickListener mImeSwitcherClickListener = new OnClickListener() { - @Override - public void onClick(View view) { - mContext.getSystemService(InputMethodManager.class).showInputMethodPickerFromSystem( - true /* showAuxiliarySubtypes */, getContext().getDisplayId()); - } - }; - private final AccessibilityDelegate mQuickStepAccessibilityDelegate = new AccessibilityDelegate() { private AccessibilityAction mToggleOverviewAction; @@ -311,32 +304,26 @@ public class NavigationBarView extends FrameLayout implements mIsVertical = false; mLongClickableAccessibilityButton = false; mNavBarMode = Dependency.get(NavigationModeController.class).addListener(this); - boolean isGesturalMode = isGesturalMode(mNavBarMode); mSysUiFlagContainer = Dependency.get(SysUiState.class); // Set up the context group of buttons mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container); final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher, mLightContext, R.drawable.ic_ime_switcher_default); - final RotationContextButton rotateSuggestionButton = new RotationContextButton( - R.id.rotate_suggestion, mLightContext, - R.drawable.ic_sysbar_rotate_button_ccw_start_0); final ContextualButton accessibilityButton = new ContextualButton(R.id.accessibility_button, mLightContext, R.drawable.ic_sysbar_accessibility_button); mContextualButtonGroup.addButton(imeSwitcherButton); - if (!isGesturalMode) { - mContextualButtonGroup.addButton(rotateSuggestionButton); - } mContextualButtonGroup.addButton(accessibilityButton); mOverviewProxyService = Dependency.get(OverviewProxyService.class); + mRotationContextButton = new RotationContextButton(R.id.rotate_suggestion, + mLightContext, R.drawable.ic_sysbar_rotate_button_ccw_start_0); mFloatingRotationButton = new FloatingRotationButton(context); mRecentsOnboarding = new RecentsOnboarding(context, mOverviewProxyService); mRotationButtonController = new RotationButtonController(mLightContext, - mLightIconColor, mDarkIconColor, - isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton, - mRotationButtonListener); + mLightIconColor, mDarkIconColor); + updateRotationButton(); mNavBarOverlayController = Dependency.get(NavigationBarOverlayController.class); if (mNavBarOverlayController.isNavigationBarOverlayEnabled()) { @@ -357,7 +344,6 @@ public class NavigationBarView extends FrameLayout implements mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps)); mButtonDispatchers.put(R.id.ime_switcher, imeSwitcherButton); mButtonDispatchers.put(R.id.accessibility_button, accessibilityButton); - mButtonDispatchers.put(R.id.rotate_suggestion, rotateSuggestionButton); mButtonDispatchers.put(R.id.menu_container, mContextualButtonGroup); mDeadZone = new DeadZone(this); @@ -555,6 +541,23 @@ public class NavigationBarView extends FrameLayout implements } } + /** + * Updates the rotation button based on the current navigation mode. + */ + private void updateRotationButton() { + if (isGesturalMode(mNavBarMode)) { + mContextualButtonGroup.removeButton(R.id.rotate_suggestion); + mButtonDispatchers.remove(R.id.rotate_suggestion); + mRotationButtonController.setRotationButton(mFloatingRotationButton, + mRotationButtonListener); + } else if (mContextualButtonGroup.getContextButton(R.id.rotate_suggestion) == null) { + mContextualButtonGroup.addButton(mRotationContextButton); + mButtonDispatchers.put(R.id.rotate_suggestion, mRotationContextButton); + mRotationButtonController.setRotationButton(mRotationContextButton, + mRotationButtonListener); + } + } + public KeyButtonDrawable getBackDrawable() { KeyButtonDrawable drawable = getDrawable(getBackDrawableRes()); orientBackButton(drawable); @@ -908,6 +911,7 @@ public class NavigationBarView extends FrameLayout implements mBarTransitions.onNavigationModeChanged(mNavBarMode); mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode); mRecentsOnboarding.onNavigationModeChanged(mNavBarMode); + updateRotationButton(); if (isGesturalMode(mNavBarMode)) { mRegionSamplingHelper.start(mSamplingBounds); @@ -932,7 +936,6 @@ public class NavigationBarView extends FrameLayout implements mNavigationInflaterView = findViewById(R.id.navigation_inflater); mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers); - getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener); updateOrientationViews(); reloadNavIcons(); } @@ -1027,6 +1030,9 @@ public class NavigationBarView extends FrameLayout implements private void updateButtonLocation(ButtonDispatcher button, boolean inScreenSpace, boolean useNearestRegion) { + if (button == null) { + return; + } View view = button.getCurrentView(); if (view == null || !button.isVisible()) { return; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java index 4bcb0193c7d0..ddf089bac25e 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/RotationButtonController.java @@ -66,10 +66,10 @@ public class RotationButtonController { private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3; private final Context mContext; - private final RotationButton mRotationButton; private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); private final UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); private final ViewRippler mViewRippler = new ViewRippler(); + private RotationButton mRotationButton; private int mLastRotationSuggestion; private boolean mPendingRotationSuggestion; @@ -125,20 +125,21 @@ public class RotationButtonController { } RotationButtonController(Context context, @ColorInt int lightIconColor, - @ColorInt int darkIconColor, RotationButton rotationButton, - Consumer<Boolean> visibilityChangedCallback) { + @ColorInt int darkIconColor) { mContext = context; mLightIconColor = lightIconColor; mDarkIconColor = darkIconColor; - mRotationButton = rotationButton; - mRotationButton.setRotationButtonController(this); mIsNavigationBarShowing = true; mRotationLockController = Dependency.get(RotationLockController.class); mAccessibilityManagerWrapper = Dependency.get(AccessibilityManagerWrapper.class); - - // Register the task stack listener mTaskStackListener = new TaskStackListenerImpl(); + } + + void setRotationButton(RotationButton rotationButton, + Consumer<Boolean> visibilityChangedCallback) { + mRotationButton = rotationButton; + mRotationButton.setRotationButtonController(this); mRotationButton.setOnClickListener(this::onRotateSuggestionClick); mRotationButton.setOnHoverListener(this::onRotateSuggestionHover); mRotationButton.setVisibilityChangedCallback(visibilityChangedCallback); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ContextualButtonGroup.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ContextualButtonGroup.java index 50b638bcc903..2ace303986fe 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ContextualButtonGroup.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/ContextualButtonGroup.java @@ -41,10 +41,23 @@ public class ContextualButtonGroup extends ButtonDispatcher { * @param button the button added to the group */ public void addButton(@NonNull ContextualButton button) { + // By default buttons in the context group are not visible until + // {@link #setButtonVisibility()) is called to show one of the buttons + button.setVisibility(View.INVISIBLE); button.attachToGroup(this); mButtonData.add(new ButtonData(button)); } + /** + * Removes a contextual button from the group. + */ + public void removeButton(@IdRes int buttonResId) { + int index = getContextButtonIndex(buttonResId); + if (index != INVALID_INDEX) { + mButtonData.remove(index); + } + } + public ContextualButton getContextButton(@IdRes int buttonResId) { int index = getContextButtonIndex(buttonResId); if (index != INVALID_INDEX) { diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java index d4ddc6546a19..a23db63a70fc 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java @@ -51,6 +51,7 @@ import android.os.Bundle; import android.text.TextUtils; import android.util.IconDrawableFactory; import android.util.Log; +import android.util.Pair; import android.view.View; import android.widget.RemoteViews; import android.widget.TextView; @@ -63,6 +64,7 @@ import com.android.systemui.people.widget.PeopleTileKey; import java.text.NumberFormat; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -102,6 +104,39 @@ public class PeopleTileViewHelper { private static final Pattern ANY_DOUBLE_MARK_PATTERN = Pattern.compile("[!?][!?]+"); private static final Pattern MIXED_MARK_PATTERN = Pattern.compile("![?].*|.*[?]!"); + // This regex can be used to match Unicode emoji characters and character sequences. It's from + // the official Unicode site (https://unicode.org/reports/tr51/#EBNF_and_Regex) with minor + // changes to fit our needs. It should be updated once new emoji categories are added. + // + // Emoji categories that can be matched by this regex: + // - Country flags. "\p{RI}\p{RI}" matches country flags since they always consist of 2 Unicode + // scalars. + // - Single-Character Emoji. "\p{Emoji}" matches Single-Character Emojis. + // - Emoji with modifiers. E.g. Emojis with different skin tones. "\p{Emoji}\p{EMod}" matches + // them. + // - Emoji Presentation. Those are characters which can normally be drawn as either text or as + // Emoji. "\p{Emoji}\x{FE0F}" matches them. + // - Emoji Keycap. E.g. Emojis for number 0 to 9. "\p{Emoji}\x{FE0F}\x{20E3}" matches them. + // - Emoji tag sequence. "\p{Emoji}[\x{E0020}-\x{E007E}]+\x{E007F}" matches them. + // - Emoji Zero-Width Joiner (ZWJ) Sequence. A ZWJ emoji is actually multiple emojis joined by + // the jointer "0x200D". + // + // Note: since "\p{Emoji}" also matches some ASCII characters like digits 0-9, we use + // "\p{Emoji}&&\p{So}" to exclude them. This is the change we made from the official emoji + // regex. + private static final String UNICODE_EMOJI_REGEX = + "\\p{RI}\\p{RI}|" + + "(" + + "\\p{Emoji}(\\p{EMod}|\\x{FE0F}\\x{20E3}?|[\\x{E0020}-\\x{E007E}]+\\x{E007F})" + + "|[\\p{Emoji}&&\\p{So}]" + + ")" + + "(" + + "\\x{200D}" + + "\\p{Emoji}(\\p{EMod}|\\x{FE0F}\\x{20E3}?|[\\x{E0020}-\\x{E007E}]+\\x{E007F})" + + "?)*"; + + private static final Pattern EMOJI_PATTERN = Pattern.compile(UNICODE_EMOJI_REGEX); + public static final String EMPTY_STRING = ""; private int mMediumVerticalPadding; @@ -375,7 +410,7 @@ public class PeopleTileViewHelper { } else { setMaxLines(views, !TextUtils.isEmpty(sender)); CharSequence content = mTile.getNotificationContent(); - views = setPunctuationRemoteViewsFields(views, content); + views = decorateBackground(views, content); views.setColorAttr(R.id.text_content, "setTextColor", android.R.attr.textColorPrimary); views.setTextViewText(R.id.text_content, mTile.getNotificationContent()); views.setViewVisibility(R.id.image, View.GONE); @@ -506,33 +541,53 @@ public class PeopleTileViewHelper { } } - private RemoteViews setPunctuationRemoteViewsFields( - RemoteViews views, CharSequence content) { - String punctuation = getBackgroundTextFromMessage(content.toString()); + private RemoteViews decorateBackground(RemoteViews views, CharSequence content) { int visibility = View.GONE; - if (punctuation != null) { - visibility = View.VISIBLE; - } - views.setTextViewText(R.id.punctuation1, punctuation); - views.setTextViewText(R.id.punctuation2, punctuation); - views.setTextViewText(R.id.punctuation3, punctuation); - views.setTextViewText(R.id.punctuation4, punctuation); - views.setTextViewText(R.id.punctuation5, punctuation); - views.setTextViewText(R.id.punctuation6, punctuation); - - views.setViewVisibility(R.id.punctuation1, visibility); - views.setViewVisibility(R.id.punctuation2, visibility); - views.setViewVisibility(R.id.punctuation3, visibility); - views.setViewVisibility(R.id.punctuation4, visibility); - views.setViewVisibility(R.id.punctuation5, visibility); - views.setViewVisibility(R.id.punctuation6, visibility); + CharSequence emoji = getDoubleEmoji(content); + if (!TextUtils.isEmpty(emoji)) { + setEmojiBackground(views, emoji); + setPunctuationBackground(views, null); + return views; + } + + CharSequence punctuation = getDoublePunctuation(content); + setEmojiBackground(views, null); + setPunctuationBackground(views, punctuation); + return views; + } + + private RemoteViews setEmojiBackground(RemoteViews views, CharSequence content) { + if (TextUtils.isEmpty(content)) { + views.setViewVisibility(R.id.emojis, View.GONE); + return views; + } + views.setTextViewText(R.id.emoji1, content); + views.setTextViewText(R.id.emoji2, content); + views.setTextViewText(R.id.emoji3, content); + + views.setViewVisibility(R.id.emojis, View.VISIBLE); + return views; + } + + private RemoteViews setPunctuationBackground(RemoteViews views, CharSequence content) { + if (TextUtils.isEmpty(content)) { + views.setViewVisibility(R.id.punctuations, View.GONE); + return views; + } + views.setTextViewText(R.id.punctuation1, content); + views.setTextViewText(R.id.punctuation2, content); + views.setTextViewText(R.id.punctuation3, content); + views.setTextViewText(R.id.punctuation4, content); + views.setTextViewText(R.id.punctuation5, content); + views.setTextViewText(R.id.punctuation6, content); + views.setViewVisibility(R.id.punctuations, View.VISIBLE); return views; } - /** Gets character for mTile background decoration based on notification content. */ + /** Returns punctuation character(s) if {@code message} has double punctuation ("!" or "?"). */ @VisibleForTesting - String getBackgroundTextFromMessage(String message) { + CharSequence getDoublePunctuation(CharSequence message) { if (!ANY_DOUBLE_MARK_PATTERN.matcher(message).find()) { return null; } @@ -554,6 +609,48 @@ public class PeopleTileViewHelper { return "!"; } + /** Returns emoji if {@code message} has two of the same emoji in sequence. */ + @VisibleForTesting + CharSequence getDoubleEmoji(CharSequence message) { + Matcher unicodeEmojiMatcher = EMOJI_PATTERN.matcher(message); + // Stores the start and end indices of each matched emoji. + List<Pair<Integer, Integer>> emojiIndices = new ArrayList<>(); + // Stores each emoji text. + List<CharSequence> emojiTexts = new ArrayList<>(); + + // Scan message for emojis + while (unicodeEmojiMatcher.find()) { + int start = unicodeEmojiMatcher.start(); + int end = unicodeEmojiMatcher.end(); + emojiIndices.add(new Pair(start, end)); + emojiTexts.add(message.subSequence(start, end)); + } + + if (DEBUG) Log.d(TAG, "Number of emojis in the message: " + emojiIndices.size()); + if (emojiIndices.size() < 2) { + return null; + } + + for (int i = 1; i < emojiIndices.size(); ++i) { + Pair<Integer, Integer> second = emojiIndices.get(i); + Pair<Integer, Integer> first = emojiIndices.get(i - 1); + + // Check if second emoji starts right after first starts + if (second.first == first.second) { + // Check if emojis in sequence are the same + if (Objects.equals(emojiTexts.get(i), emojiTexts.get(i - 1))) { + if (DEBUG) { + Log.d(TAG, "Two of the same emojis in sequence: " + emojiTexts.get(i)); + } + return emojiTexts.get(i); + } + } + } + + // No equal emojis in sequence. + return null; + } + private RemoteViews getViewForContentLayout() { RemoteViews views = new RemoteViews(mContext.getPackageName(), getLayoutForContent()); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 2458223310cf..a9723341e787 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -95,7 +95,7 @@ public class UserDetailView extends PseudoGridView { public UserDetailItemView createUserDetailItemView(View convertView, ViewGroup parent, UserSwitcherController.UserRecord item) { UserDetailItemView v = UserDetailItemView.convertOrInflate( - mContext, convertView, parent); + parent.getContext(), convertView, parent); if (!item.isCurrent || item.isGuest) { v.setOnClickListener(this); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java index 7f31fddbfb6c..5437ce63475e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar; import static com.android.systemui.statusbar.RemoteInputController.processForRemoteInput; import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; -import static com.android.systemui.statusbar.phone.StatusBar.DEBUG; import android.annotation.NonNull; import android.annotation.SuppressLint; @@ -35,6 +34,7 @@ import android.util.Log; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins; +import com.android.systemui.statusbar.phone.StatusBar; import java.util.ArrayList; import java.util.List; @@ -46,6 +46,7 @@ import java.util.List; @SuppressLint("OverrideAbstract") public class NotificationListener extends NotificationListenerWithPlugins { private static final String TAG = "NotificationListener"; + private static final boolean DEBUG = StatusBar.DEBUG; private final Context mContext; private final NotificationManager mNotificationManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index fb109f310e15..8f462fea0468 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -84,7 +84,7 @@ public class NotificationShelf extends ActivatableNotificationView implements private int mCutoutHeight; private int mGapHeight; private int mIndexOfFirstViewInShelf = -1; - + private float mCornerAnimationDistance; private NotificationShelfController mController; public NotificationShelf(Context context, AttributeSet attrs) { @@ -104,6 +104,7 @@ public class NotificationShelf extends ActivatableNotificationView implements setClipToPadding(false); mShelfIcons.setIsStaticLayout(false); setBottomRoundness(1.0f, false /* animate */); + setTopRoundness(1f, false /* animate */); // Setting this to first in section to get the clipping to the top roundness correct. This // value determines the way we are clipping to the top roundness of the overall shade @@ -134,6 +135,8 @@ public class NotificationShelf extends ActivatableNotificationView implements mIconSize = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_icon_size); mHiddenShelfIconSize = res.getDimensionPixelOffset(R.dimen.hidden_shelf_icon_size); mGapHeight = res.getDimensionPixelSize(R.dimen.qs_notification_padding); + mCornerAnimationDistance = res.getDimensionPixelSize( + R.dimen.notification_corner_animation_distance); mShelfIcons.setInNotificationIconShelf(true); if (!mShowNotificationShelf) { @@ -256,7 +259,7 @@ public class NotificationShelf extends ActivatableNotificationView implements boolean aboveShelf = ViewState.getFinalTranslationZ(child) > baseZHeight || child.isPinned(); boolean isLastChild = child == lastChild; - float rowTranslationY = child.getTranslationY(); + final float viewStart = child.getTranslationY(); final float inShelfAmount = updateShelfTransformation(i, child, scrollingFast, expandingAnimated, isLastChild); @@ -278,7 +281,7 @@ public class NotificationShelf extends ActivatableNotificationView implements ExpandableNotificationRow expandableRow = (ExpandableNotificationRow) child; numViewsInShelf += inShelfAmount; int ownColorUntinted = expandableRow.getBackgroundColorWithoutTint(); - if (rowTranslationY >= shelfStart && mNotGoneIndex == -1) { + if (viewStart >= shelfStart && mNotGoneIndex == -1) { mNotGoneIndex = notGoneIndex; setTintColor(previousColor); setOverrideTintColor(colorTwoBefore, transitionAmount); @@ -317,26 +320,44 @@ public class NotificationShelf extends ActivatableNotificationView implements notGoneIndex++; } + final float viewEnd = viewStart + child.getActualHeight(); + final float cornerAnimationDistance = mCornerAnimationDistance + * mAmbientState.getExpansionFraction(); + final float cornerAnimationTop = shelfStart - cornerAnimationDistance; + if (child instanceof ActivatableNotificationView) { ActivatableNotificationView anv = (ActivatableNotificationView) child; - if (anv.isFirstInSection() && previousAnv != null - && previousAnv.isLastInSection()) { - // If the top of the shelf is between the view before a gap and the view after a - // gap then we need to adjust the shelf's top roundness. - float distanceToGapBottom = child.getTranslationY() - getTranslationY(); - float distanceToGapTop = getTranslationY() - - (previousAnv.getTranslationY() + previousAnv.getActualHeight()); - if (distanceToGapTop > 0) { - // We interpolate our top roundness so that it's fully rounded if we're at - // the bottom of the gap, and not rounded at all if we're at the top of the - // gap (directly up against the bottom of previousAnv) - // Then we apply the same roundness to the bottom of previousAnv so that the - // corners join together as the shelf approaches previousAnv. - firstElementRoundness = (float) Math.min(1.0, - distanceToGapTop / mGapHeight); - previousAnv.setBottomRoundness(firstElementRoundness, - false /* don't animate */); + + if (viewStart < shelfStart + && !mHostLayoutController.isViewAffectedBySwipe(anv) + && !mAmbientState.isPulsing() + && !mAmbientState.isDozing()) { + + if (viewEnd >= cornerAnimationTop) { + // Round bottom corners within animation bounds + final float changeFraction = MathUtils.saturate( + (viewEnd - cornerAnimationTop) / cornerAnimationDistance); + final float roundness = anv.isLastInSection() ? 1f : changeFraction * 1f; + anv.setBottomRoundness(roundness, false); + + } else if (viewEnd < cornerAnimationTop) { + // Fast scroll skips frames and leaves corners with unfinished rounding. + // Reset top and bottom corners outside of animation bounds. + anv.setBottomRoundness(anv.isLastInSection() ? 1f : 0f, false); + } + + if (viewStart >= cornerAnimationTop) { + // Round top corners within animation bounds + final float changeFraction = MathUtils.saturate( + (viewStart - cornerAnimationTop) / cornerAnimationDistance); + final float roundness = anv.isFirstInSection() ? 1f : changeFraction * 1f; + anv.setTopRoundness(roundness, false); + + } else if (viewStart < cornerAnimationTop) { + // Fast scroll skips frames and leaves corners with unfinished rounding. + // Reset top and bottom corners outside of animation bounds. + anv.setTopRoundness(anv.isFirstInSection() ? 1f : 0f, false); } } previousAnv = anv; @@ -394,7 +415,6 @@ public class NotificationShelf extends ActivatableNotificationView implements private void setFirstElementRoundness(float firstElementRoundness) { if (mFirstElementRoundness != firstElementRoundness) { mFirstElementRoundness = firstElementRoundness; - setTopRoundness(firstElementRoundness, false /* animate */); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt index c85b62fc0d9a..81942209a055 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt @@ -6,6 +6,7 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.statusbar.NotificationShadeDepthController import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.stack.NotificationListContainer +import com.android.systemui.statusbar.phone.HeadsUpManagerPhone import com.android.systemui.statusbar.phone.NotificationShadeWindowViewController import kotlin.math.ceil import kotlin.math.max @@ -14,7 +15,8 @@ import kotlin.math.max class NotificationLaunchAnimatorControllerProvider( private val notificationShadeWindowViewController: NotificationShadeWindowViewController, private val notificationListContainer: NotificationListContainer, - private val depthController: NotificationShadeDepthController + private val depthController: NotificationShadeDepthController, + private val headsUpManager: HeadsUpManagerPhone ) { fun getAnimatorController( notification: ExpandableNotificationRow @@ -23,7 +25,8 @@ class NotificationLaunchAnimatorControllerProvider( notificationShadeWindowViewController, notificationListContainer, depthController, - notification + notification, + headsUpManager ) } } @@ -37,8 +40,11 @@ class NotificationLaunchAnimatorController( private val notificationShadeWindowViewController: NotificationShadeWindowViewController, private val notificationListContainer: NotificationListContainer, private val depthController: NotificationShadeDepthController, - private val notification: ExpandableNotificationRow + private val notification: ExpandableNotificationRow, + private val headsUpManager: HeadsUpManagerPhone ) : ActivityLaunchAnimator.Controller { + private val notificationKey = notification.entry.sbn.key + override fun getRootView(): View = notification.rootView override fun createAnimatorState(): ActivityLaunchAnimator.State { @@ -76,12 +82,25 @@ class NotificationLaunchAnimatorController( override fun onIntentStarted(willAnimate: Boolean) { notificationShadeWindowViewController.setExpandAnimationRunning(willAnimate) + + if (!willAnimate) { + removeHun(animate = true) + } + } + + private fun removeHun(animate: Boolean) { + if (!headsUpManager.isAlerting(notificationKey)) { + return + } + + headsUpManager.removeNotification(notificationKey, true /* releaseImmediately */, animate) } override fun onLaunchAnimationCancelled() { // TODO(b/184121838): Should we call InteractionJankMonitor.cancel if the animation started // here? notificationShadeWindowViewController.setExpandAnimationRunning(false) + removeHun(animate = true) } override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { @@ -99,6 +118,7 @@ class NotificationLaunchAnimatorController( notificationShadeWindowViewController.setExpandAnimationRunning(false) notificationListContainer.setExpandingNotification(null) applyParams(null) + removeHun(animate = false) } private fun applyParams(params: ExpandAnimationParameters?) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java index d6356de5ea51..d95c265c1460 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.notification.collection.legacy; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Notification; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; @@ -31,6 +33,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.wm.shell.bubbles.Bubbles; @@ -39,10 +42,12 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.TreeSet; import javax.inject.Inject; @@ -58,13 +63,21 @@ import dagger.Lazy; public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, StateListener, GroupMembershipManager, GroupExpansionManager, Dumpable { - private static final String TAG = "NotificationGroupManager"; + private static final String TAG = "NotifGroupManager"; + private static final boolean DEBUG = StatusBar.DEBUG; + private static final boolean SPEW = StatusBar.SPEW; + /** + * The maximum amount of time (in ms) between the posting of notifications that can be + * considered part of the same update batch. + */ + private static final long POST_BATCH_MAX_AGE = 5000; private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>(); private final ArraySet<OnGroupExpansionChangeListener> mExpansionChangeListeners = new ArraySet<>(); private final ArraySet<OnGroupChangeListener> mGroupChangeListeners = new ArraySet<>(); private final Lazy<PeopleNotificationIdentifier> mPeopleNotificationIdentifier; private final Optional<Bubbles> mBubblesOptional; + private final EventBuffer mEventBuffer = new EventBuffer(); private int mBarState = -1; private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>(); private HeadsUpManager mHeadsUpManager; @@ -134,8 +147,14 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, * When we want to remove an entry from being tracked for grouping */ public void onEntryRemoved(NotificationEntry removed) { + if (SPEW) { + Log.d(TAG, "onEntryRemoved: entry=" + removed); + } onEntryRemovedInternal(removed, removed.getSbn()); - mIsolatedEntries.remove(removed.getKey()); + StatusBarNotification oldSbn = mIsolatedEntries.remove(removed.getKey()); + if (oldSbn != null) { + updateSuppression(mGroupMap.get(oldSbn.getGroupKey())); + } } /** @@ -162,6 +181,9 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, // the close future. See b/23676310 for reference. return; } + if (SPEW) { + Log.d(TAG, "onEntryRemovedInternal: entry=" + removed + " group=" + group.groupKey); + } if (isGroupChild(removed.getKey(), isGroup, isGroupSummary)) { group.children.remove(removed.getKey()); } else { @@ -182,6 +204,9 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, * Notify the group manager that a new entry was added */ public void onEntryAdded(final NotificationEntry added) { + if (SPEW) { + Log.d(TAG, "onEntryAdded: entry=" + added); + } updateIsolation(added); onEntryAddedInternal(added); } @@ -195,13 +220,16 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, String groupKey = getGroupKey(sbn); NotificationGroup group = mGroupMap.get(groupKey); if (group == null) { - group = new NotificationGroup(); + group = new NotificationGroup(groupKey); mGroupMap.put(groupKey, group); for (OnGroupChangeListener listener : mGroupChangeListeners) { listener.onGroupCreated(group, groupKey); } } + if (SPEW) { + Log.d(TAG, "onEntryAddedInternal: entry=" + added + " group=" + group.groupKey); + } if (isGroupChild) { NotificationEntry existing = group.children.get(added.getKey()); if (existing != null && existing != added) { @@ -213,9 +241,11 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, + " added removed" + added.isRowRemoved(), new Throwable()); } group.children.put(added.getKey(), added); + addToPostBatchHistory(group, added); updateSuppression(group); } else { group.summary = added; + addToPostBatchHistory(group, added); group.expanded = added.areChildrenExpanded(); updateSuppression(group); if (!group.children.isEmpty()) { @@ -231,6 +261,27 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, } } + private void addToPostBatchHistory(NotificationGroup group, @Nullable NotificationEntry entry) { + if (entry == null) { + return; + } + boolean didAdd = group.postBatchHistory.add(new PostRecord(entry)); + if (didAdd) { + trimPostBatchHistory(group.postBatchHistory); + } + } + + /** remove all history that's too old to be in the batch. */ + private void trimPostBatchHistory(@NonNull TreeSet<PostRecord> postBatchHistory) { + if (postBatchHistory.size() <= 1) { + return; + } + long batchStartTime = postBatchHistory.last().postTime - POST_BATCH_MAX_AGE; + while (!postBatchHistory.isEmpty() && postBatchHistory.first().postTime < batchStartTime) { + postBatchHistory.pollFirst(); + } + } + private void onEntryBecomingChild(NotificationEntry entry) { updateIsolation(entry); } @@ -239,6 +290,9 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, if (group == null) { return; } + NotificationEntry prevAlertOverride = group.alertOverride; + group.alertOverride = getPriorityConversationAlertOverride(group); + int childCount = 0; boolean hasBubbles = false; for (NotificationEntry entry : group.children.values()) { @@ -255,18 +309,148 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, group.suppressed = group.summary != null && !group.expanded && (childCount == 1 || (childCount == 0 - && group.summary.getSbn().getNotification().isGroupSummary() - && (hasIsolatedChildren(group) || hasBubbles))); - if (prevSuppressed != group.suppressed) { - for (OnGroupChangeListener listener : mGroupChangeListeners) { - if (!mIsUpdatingUnchangedGroup) { - listener.onGroupSuppressionChanged(group, group.suppressed); - listener.onGroupsChanged(); + && group.summary.getSbn().getNotification().isGroupSummary() + && (hasIsolatedChildren(group) || hasBubbles))); + + boolean alertOverrideChanged = prevAlertOverride != group.alertOverride; + boolean suppressionChanged = prevSuppressed != group.suppressed; + if (alertOverrideChanged || suppressionChanged) { + if (DEBUG && alertOverrideChanged) { + Log.d(TAG, group + " alertOverride was=" + prevAlertOverride + " now=" + + group.alertOverride); + } + if (DEBUG && suppressionChanged) { + Log.d(TAG, group + " suppressed changed to " + group.suppressed); + } + if (!mIsUpdatingUnchangedGroup) { + if (alertOverrideChanged) { + mEventBuffer.notifyAlertOverrideChanged(group, prevAlertOverride); + } + if (suppressionChanged) { + for (OnGroupChangeListener listener : mGroupChangeListeners) { + listener.onGroupSuppressionChanged(group, group.suppressed); + } + } + mEventBuffer.notifyGroupsChanged(); + } else { + if (DEBUG) { + Log.d(TAG, group + " did not notify listeners of above change(s)"); } } } } + /** + * Finds the isolated logical child of this group which is should be alerted instead. + * + * Notifications from priority conversations are isolated from their groups to make them more + * prominent, however apps may post these with a GroupAlertBehavior that has the group receiving + * the alert. This would lead to the group alerting even though the conversation that was + * updated was not actually a part of that group. This method finds the best priority + * conversation in this situation, if there is one, so they can be set as the alertOverride of + * the group. + * + * @param group the group to check + * @return the entry which should receive the alert instead of the group, if any. + */ + @Nullable + private NotificationEntry getPriorityConversationAlertOverride(NotificationGroup group) { + // GOAL: if there is a priority child which wouldn't alert based on its groupAlertBehavior, + // but which should be alerting (because priority conversations are isolated), find it. + if (group == null || group.summary == null) { + if (SPEW) { + Log.d(TAG, "getPriorityConversationAlertOverride: null group or summary"); + } + return null; + } + if (isIsolated(group.summary.getKey())) { + if (SPEW) { + Log.d(TAG, "getPriorityConversationAlertOverride: isolated group"); + } + return null; + } + + // Precondiions: + // * Only necessary when all notifications in the group use GROUP_ALERT_SUMMARY + // * Only necessary when at least one notification in the group is on a priority channel + if (group.summary.getSbn().getNotification().getGroupAlertBehavior() + != Notification.GROUP_ALERT_SUMMARY) { + if (SPEW) { + Log.d(TAG, "getPriorityConversationAlertOverride: summary != GROUP_ALERT_SUMMARY"); + } + return null; + } + + // Get the important children first, copy the keys for the final importance check, + // then add the non-isolated children to the map for unified lookup. + HashMap<String, NotificationEntry> children = getImportantConversations(group); + if (children == null || children.isEmpty()) { + if (SPEW) { + Log.d(TAG, "getPriorityConversationAlertOverride: no important conversations"); + } + return null; + } + HashSet<String> importantChildKeys = new HashSet<>(children.keySet()); + children.putAll(group.children); + + // Ensure all children have GROUP_ALERT_SUMMARY + for (NotificationEntry child : children.values()) { + if (child.getSbn().getNotification().getGroupAlertBehavior() + != Notification.GROUP_ALERT_SUMMARY) { + if (SPEW) { + Log.d(TAG, "getPriorityConversationAlertOverride: " + + "child != GROUP_ALERT_SUMMARY"); + } + return null; + } + } + + // Create a merged post history from all the children + TreeSet<PostRecord> combinedHistory = new TreeSet<>(group.postBatchHistory); + for (String importantChildKey : importantChildKeys) { + NotificationGroup importantChildGroup = mGroupMap.get(importantChildKey); + combinedHistory.addAll(importantChildGroup.postBatchHistory); + } + trimPostBatchHistory(combinedHistory); + + // This is a streamlined implementation of the following idea: + // * From the subset of notifications in the latest 'batch' of updates. A batch is: + // * Notifs posted less than POST_BATCH_MAX_AGE before the most recently posted. + // * Only including notifs newer than the second-to-last post of any notification. + // * Find the newest child in the batch -- the with the largest 'when' value. + // * If the newest child is a priority conversation, set that as the override. + HashSet<String> batchKeys = new HashSet<>(); + long newestChildWhen = -1; + NotificationEntry newestChild = null; + // Iterate backwards through the post history, tracking the child with the smallest sort key + for (PostRecord record : combinedHistory.descendingSet()) { + if (batchKeys.contains(record.key)) { + // Once you see a notification again, the batch has ended + break; + } + batchKeys.add(record.key); + NotificationEntry child = children.get(record.key); + if (child != null) { + long childWhen = child.getSbn().getNotification().when; + if (newestChild == null || childWhen > newestChildWhen) { + newestChildWhen = childWhen; + newestChild = child; + } + } + } + if (newestChild != null && importantChildKeys.contains(newestChild.getKey())) { + if (SPEW) { + Log.d(TAG, "getPriorityConversationAlertOverride: result=" + newestChild); + } + return newestChild; + } + if (SPEW) { + Log.d(TAG, "getPriorityConversationAlertOverride: result=null, newestChild=" + + newestChild); + } + return null; + } + private boolean hasIsolatedChildren(NotificationGroup group) { return getNumberOfIsolatedChildren(group.summary.getSbn().getGroupKey()) != 0; } @@ -281,12 +465,33 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, return count; } + @Nullable + private HashMap<String, NotificationEntry> getImportantConversations(NotificationGroup group) { + String groupKey = group.summary.getSbn().getGroupKey(); + HashMap<String, NotificationEntry> result = null; + for (StatusBarNotification sbn : mIsolatedEntries.values()) { + if (sbn.getGroupKey().equals(groupKey)) { + NotificationEntry entry = mGroupMap.get(sbn.getKey()).summary; + if (isImportantConversation(entry)) { + if (result == null) { + result = new HashMap<>(); + } + result.put(sbn.getKey(), entry); + } + } + } + return result; + } + /** * Update an entry's group information * @param entry notification entry to update * @param oldNotification previous notification info before this update */ public void onEntryUpdated(NotificationEntry entry, StatusBarNotification oldNotification) { + if (SPEW) { + Log.d(TAG, "onEntryUpdated: entry=" + entry); + } onEntryUpdated(entry, oldNotification.getGroupKey(), oldNotification.isGroup(), oldNotification.getNotification().isGroupSummary()); } @@ -325,7 +530,17 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, * Whether the given notification is the summary of a group that is being suppressed */ public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) { - return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary(); + return sbn.getNotification().isGroupSummary() && isGroupSuppressed(getGroupKey(sbn)); + } + + /** + * If the given notification is a summary, get the group for it. + */ + public NotificationGroup getGroupForSummary(StatusBarNotification sbn) { + if (sbn.getNotification().isGroupSummary()) { + return mGroupMap.get(getGroupKey(sbn)); + } + return null; } private boolean isOnlyChild(StatusBarNotification sbn) { @@ -545,9 +760,7 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, if (!sbn.isGroup() || sbn.getNotification().isGroupSummary()) { return false; } - int peopleNotificationType = - mPeopleNotificationIdentifier.get().getPeopleNotificationType(entry); - if (peopleNotificationType == PeopleNotificationIdentifier.TYPE_IMPORTANT_PERSON) { + if (isImportantConversation(entry)) { return true; } if (mHeadsUpManager != null && !mHeadsUpManager.isAlerting(entry.getKey())) { @@ -560,18 +773,25 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, || isGroupNotFullyVisible(notificationGroup)); } + private boolean isImportantConversation(NotificationEntry entry) { + int peopleNotificationType = + mPeopleNotificationIdentifier.get().getPeopleNotificationType(entry); + return peopleNotificationType == PeopleNotificationIdentifier.TYPE_IMPORTANT_PERSON; + } + /** * Isolate a notification from its group so that it visually shows as its own group. * * @param entry the notification to isolate */ private void isolateNotification(NotificationEntry entry) { - StatusBarNotification sbn = entry.getSbn(); - + if (SPEW) { + Log.d(TAG, "isolateNotification: entry=" + entry); + } // We will be isolated now, so lets update the groups onEntryRemovedInternal(entry, entry.getSbn()); - mIsolatedEntries.put(sbn.getKey(), sbn); + mIsolatedEntries.put(entry.getKey(), entry.getSbn()); onEntryAddedInternal(entry); // We also need to update the suppression of the old group, because this call comes @@ -588,6 +808,14 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, * Update the isolation of an entry, splitting it from the group. */ public void updateIsolation(NotificationEntry entry) { + // We need to buffer a few events because we do isolation changes in 3 steps: + // removeInternal, update mIsolatedEntries, addInternal. This means that often the + // alertOverride will update on the removal, however processing the event in that case can + // cause problems because the mIsolatedEntries map is not in its final state, so the event + // listener may be unable to correctly determine the true state of the group. By delaying + // the alertOverride change until after the add phase, we can ensure that listeners only + // have to handle a consistent state. + mEventBuffer.startBuffering(); boolean isIsolated = isIsolated(entry.getSbn().getKey()); if (shouldIsolate(entry)) { if (!isIsolated) { @@ -596,6 +824,7 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, } else if (isIsolated) { stopIsolatingNotification(entry); } + mEventBuffer.flushAndStopBuffering(); } /** @@ -604,15 +833,15 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, * @param entry the notification to un-isolate */ private void stopIsolatingNotification(NotificationEntry entry) { - StatusBarNotification sbn = entry.getSbn(); - if (isIsolated(sbn.getKey())) { - // not isolated anymore, we need to update the groups - onEntryRemovedInternal(entry, entry.getSbn()); - mIsolatedEntries.remove(sbn.getKey()); - onEntryAddedInternal(entry); - for (OnGroupChangeListener listener : mGroupChangeListeners) { - listener.onGroupsChanged(); - } + if (SPEW) { + Log.d(TAG, "stopIsolatingNotification: entry=" + entry); + } + // not isolated anymore, we need to update the groups + onEntryRemovedInternal(entry, entry.getSbn()); + mIsolatedEntries.remove(entry.getKey()); + onEntryAddedInternal(entry); + for (OnGroupChangeListener listener : mGroupChangeListeners) { + listener.onGroupsChanged(); } } @@ -648,33 +877,154 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, } /** + * A record of a notification being posted, containing the time of the post and the key of the + * notification entry. These are stored in a TreeSet by the NotificationGroup and used to + * calculate a batch of notifications. + */ + public static class PostRecord implements Comparable<PostRecord> { + public final long postTime; + public final String key; + + /** constructs a record containing the post time and key from the notification entry */ + public PostRecord(@NonNull NotificationEntry entry) { + this.postTime = entry.getSbn().getPostTime(); + this.key = entry.getKey(); + } + + @Override + public int compareTo(PostRecord o) { + int postTimeComparison = Long.compare(this.postTime, o.postTime); + return postTimeComparison == 0 + ? String.CASE_INSENSITIVE_ORDER.compare(this.key, o.key) + : postTimeComparison; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PostRecord that = (PostRecord) o; + return postTime == that.postTime && key.equals(that.key); + } + + @Override + public int hashCode() { + return Objects.hash(postTime, key); + } + } + + /** * Represents a notification group in the notification shade. */ public static class NotificationGroup { + public final String groupKey; public final HashMap<String, NotificationEntry> children = new HashMap<>(); + public final TreeSet<PostRecord> postBatchHistory = new TreeSet<>(); public NotificationEntry summary; public boolean expanded; /** * Is this notification group suppressed, i.e its summary is hidden */ public boolean suppressed; + /** + * The child (which is isolated from this group) to which the alert should be transferred, + * due to priority conversations. + */ + public NotificationEntry alertOverride; + + NotificationGroup(String groupKey) { + this.groupKey = groupKey; + } @Override public String toString() { - String result = " summary:\n " - + (summary != null ? summary.getSbn() : "null") - + (summary != null && summary.getDebugThrowable() != null - ? Log.getStackTraceString(summary.getDebugThrowable()) - : ""); - result += "\n children size: " + children.size(); + StringBuilder sb = new StringBuilder(); + sb.append(" groupKey: ").append(groupKey); + sb.append("\n summary:"); + appendEntry(sb, summary); + sb.append("\n children size: ").append(children.size()); for (NotificationEntry child : children.values()) { - result += "\n " + child.getSbn() - + (child.getDebugThrowable() != null - ? Log.getStackTraceString(child.getDebugThrowable()) - : ""); + appendEntry(sb, child); + } + sb.append("\n alertOverride:"); + appendEntry(sb, alertOverride); + sb.append("\n summary suppressed: ").append(suppressed); + return sb.toString(); + } + + private void appendEntry(StringBuilder sb, NotificationEntry entry) { + sb.append("\n ").append(entry != null ? entry.getSbn() : "null"); + if (entry != null && entry.getDebugThrowable() != null) { + sb.append(Log.getStackTraceString(entry.getDebugThrowable())); + } + } + } + + /** + * This class is a toggleable buffer for a subset of events of {@link OnGroupChangeListener}. + * When buffering, instead of notifying the listeners it will set internal state that will allow + * it to notify listeners of those events later + */ + private class EventBuffer { + private final HashMap<String, NotificationEntry> mOldAlertOverrideByGroup = new HashMap<>(); + private boolean mIsBuffering = false; + private boolean mDidGroupsChange = false; + + void notifyAlertOverrideChanged(NotificationGroup group, + NotificationEntry oldAlertOverride) { + if (mIsBuffering) { + // The value in this map is the override before the event. If there is an entry + // already in the map, then we are effectively coalescing two events, which means + // we need to preserve the original initial value. + mOldAlertOverrideByGroup.putIfAbsent(group.groupKey, oldAlertOverride); + } else { + for (OnGroupChangeListener listener : mGroupChangeListeners) { + listener.onGroupAlertOverrideChanged(group, oldAlertOverride, + group.alertOverride); + } + } + } + + void notifyGroupsChanged() { + if (mIsBuffering) { + mDidGroupsChange = true; + } else { + for (OnGroupChangeListener listener : mGroupChangeListeners) { + listener.onGroupsChanged(); + } + } + } + + void startBuffering() { + mIsBuffering = true; + } + + void flushAndStopBuffering() { + // stop buffering so that we can call our own helpers + mIsBuffering = false; + // alert all group alert override changes for groups that were not removed + for (Map.Entry<String, NotificationEntry> entry : mOldAlertOverrideByGroup.entrySet()) { + NotificationGroup group = mGroupMap.get(entry.getKey()); + if (group == null) { + // The group can be null if this alertOverride changed before the group was + // permanently removed, meaning that there's no guarantee that listeners will + // that field clear. + continue; + } + NotificationEntry oldAlertOverride = entry.getValue(); + if (group.alertOverride == oldAlertOverride) { + // If the final alertOverride equals the initial, it means we coalesced two + // events which undid the change, so we can drop it entirely. + continue; + } + notifyAlertOverrideChanged(group, oldAlertOverride); + } + mOldAlertOverrideByGroup.clear(); + // alert that groups changed + if (mDidGroupsChange) { + notifyGroupsChanged(); + mDidGroupsChange = false; } - result += "\n summary suppressed: " + suppressed; - return result; } } @@ -714,6 +1064,18 @@ public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, boolean suppressed) {} /** + * The alert override of a group has changed. + * + * @param group the group that has changed + * @param oldAlertOverride the previous notification to which the group's alerts were sent + * @param newAlertOverride the notification to which the group's alerts should now be sent + */ + default void onGroupAlertOverrideChanged( + NotificationGroup group, + @Nullable NotificationEntry oldAlertOverride, + @Nullable NotificationEntry newAlertOverride) {} + + /** * A group of children just received a summary notification and should therefore become * children of it. * diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java index af8b4d99792a..f6ab409998bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java @@ -84,8 +84,7 @@ public class NotificationBackgroundView extends View { int bottom = mActualHeight; if (mBottomIsRounded && mBottomAmountClips - && !mExpandAnimationRunning - && !mLastInSection) { + && !mExpandAnimationRunning) { bottom -= mClipBottomAmount; } int left = 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java index b1ac12e84fb3..4b49e3a90ede 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java @@ -71,6 +71,13 @@ public class NotificationRoundnessManager { } } + public boolean isViewAffectedBySwipe(ExpandableView expandableView) { + return expandableView != null + && (expandableView == mSwipedView + || expandableView == mViewBeforeSwipedView + || expandableView == mViewAfterSwipedView); + } + boolean updateViewWithoutCallback(ExpandableView view, boolean animate) { if (view == null @@ -78,38 +85,35 @@ public class NotificationRoundnessManager { || view == mViewAfterSwipedView) { return false; } - float topRoundness = getRoundness(view, true /* top */); - float bottomRoundness = getRoundness(view, false /* top */); - boolean topChanged = view.setTopRoundness(topRoundness, animate); - boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate); - boolean firstInSection = isFirstInSection(view, false /* exclude first section */); - boolean lastInSection = isLastInSection(view, false /* exclude last section */); - view.setFirstInSection(firstInSection); - view.setLastInSection(lastInSection); - return (firstInSection || lastInSection) && (topChanged || bottomChanged); + + final float topRoundness = getRoundness(view, true /* top */); + final float bottomRoundness = getRoundness(view, false /* top */); + + final boolean topChanged = view.setTopRoundness(topRoundness, animate); + final boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate); + + final boolean isFirstInSection = isFirstInSection(view); + final boolean isLastInSection = isLastInSection(view); + + view.setFirstInSection(isFirstInSection); + view.setLastInSection(isLastInSection); + + return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged); } - private boolean isFirstInSection(ExpandableView view, boolean includeFirstSection) { - int numNonEmptySections = 0; + private boolean isFirstInSection(ExpandableView view) { for (int i = 0; i < mFirstInSectionViews.length; i++) { if (view == mFirstInSectionViews[i]) { - return includeFirstSection || numNonEmptySections > 0; - } - if (mFirstInSectionViews[i] != null) { - numNonEmptySections++; + return true; } } return false; } - private boolean isLastInSection(ExpandableView view, boolean includeLastSection) { - int numNonEmptySections = 0; + private boolean isLastInSection(ExpandableView view) { for (int i = mLastInSectionViews.length - 1; i >= 0; i--) { if (view == mLastInSectionViews[i]) { - return includeLastSection || numNonEmptySections > 0; - } - if (mLastInSectionViews[i] != null) { - numNonEmptySections++; + return true; } } return false; @@ -172,10 +176,10 @@ public class NotificationRoundnessManager { || (view.isHeadsUpAnimatingAway()) && !mExpanded)) { return 1.0f; } - if (isFirstInSection(view, true /* include first section */) && top) { + if (isFirstInSection(view) && top) { return 1.0f; } - if (isLastInSection(view, true /* include last section */) && !top) { + if (isLastInSection(view) && !top) { return 1.0f; } if (view == mTrackedHeadsUp) { @@ -229,10 +233,8 @@ public class NotificationRoundnessManager { : section.getLastVisibleChild()); if (newView == oldView) { isStillPresent = true; - if (oldView.isFirstInSection() != isFirstInSection(oldView, - false /* exclude first section */) - || oldView.isLastInSection() != isLastInSection(oldView, - false /* exclude last section */)) { + if (oldView.isFirstInSection() != isFirstInSection(oldView) + || oldView.isLastInSection() != isLastInSection(oldView)) { adjacentSectionChanged = true; } break; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index ae9467eb651b..b039df3f32af 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -740,6 +740,10 @@ public class NotificationStackScrollLayoutController { return true; } + public boolean isViewAffectedBySwipe(ExpandableView expandableView) { + return mNotificationRoundnessManager.isViewAffectedBySwipe(expandableView); + } + public void addOnExpandedHeightChangedListener(BiConsumer<Float, Float> listener) { mView.addOnExpandedHeightChangedListener(listener); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index 3827123f0160..4b545ebf2a05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -301,7 +301,7 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, } /////////////////////////////////////////////////////////////////////////////////////////////// - // HeadsUpManager public methods overrides: + // HeadsUpManager public methods overrides and overloads: @Override public boolean isTrackingHeadsUp() { @@ -318,6 +318,18 @@ public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, mSwipedOutKeys.add(key); } + public boolean removeNotification(@NonNull String key, boolean releaseImmediately, + boolean animate) { + if (animate) { + return removeNotification(key, releaseImmediately); + } else { + mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); + boolean removed = removeNotification(key, releaseImmediately); + mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); + return removed; + } + } + /////////////////////////////////////////////////////////////////////////////////////////////// // Dumpable overrides: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java index 3181f520dca2..9787a9446019 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelper.java @@ -22,12 +22,12 @@ import android.app.Notification; import android.os.SystemClock; import android.service.notification.StatusBarNotification; import android.util.ArrayMap; +import android.util.Log; import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.Dependency; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; -import com.android.systemui.statusbar.AlertingNotificationManager; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -41,17 +41,21 @@ import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** * A helper class dealing with the alert interactions between {@link NotificationGroupManagerLegacy} * and {@link HeadsUpManager}. In particular, this class deals with keeping - * the correct notification in a group alerting based off the group suppression. + * the correct notification in a group alerting based off the group suppression and alertOverride. */ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedListener, StateListener { private static final long ALERT_TRANSFER_TIMEOUT = 300; + private static final String TAG = "NotifGroupAlertTransfer"; + private static final boolean DEBUG = StatusBar.DEBUG; + private static final boolean SPEW = StatusBar.SPEW; /** * The list of entries containing group alert metadata for each group. Keyed by group key. @@ -142,41 +146,98 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis @Override public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) { - if (suppressed) { - if (mHeadsUpManager.isAlerting(group.summary.getKey())) { - handleSuppressedSummaryAlerted(group.summary, mHeadsUpManager); + if (DEBUG) { + Log.d(TAG, "!! onGroupSuppressionChanged: group.summary=" + group.summary + + " suppressed=" + suppressed); + } + NotificationEntry oldAlertOverride = group.alertOverride; + onGroupChanged(group, oldAlertOverride); + } + + @Override + public void onGroupAlertOverrideChanged(NotificationGroup group, + @Nullable NotificationEntry oldAlertOverride, + @Nullable NotificationEntry newAlertOverride) { + if (DEBUG) { + Log.d(TAG, "!! onGroupAlertOverrideChanged: group.summary=" + group.summary + + " oldAlertOverride=" + oldAlertOverride + + " newAlertOverride=" + newAlertOverride); + } + onGroupChanged(group, oldAlertOverride); + } + }; + + /** + * Called when either the suppressed or alertOverride fields of the group changed + * + * @param group the group which changed + * @param oldAlertOverride the previous value of group.alertOverride + */ + private void onGroupChanged(NotificationGroup group, + NotificationEntry oldAlertOverride) { + // Group summary can be null if we are no longer suppressed because the summary was + // removed. In that case, we don't need to alert the summary. + if (group.summary == null) { + if (DEBUG) { + Log.d(TAG, "onGroupChanged: summary is null"); + } + return; + } + if (group.suppressed || group.alertOverride != null) { + checkForForwardAlertTransfer(group.summary, oldAlertOverride); + } else { + if (DEBUG) { + Log.d(TAG, "onGroupChanged: maybe transfer back"); + } + GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey( + group.summary.getSbn())); + // Group is no longer suppressed or overridden. + // We should check if we need to transfer the alert back to the summary. + if (groupAlertEntry.mAlertSummaryOnNextAddition) { + if (!mHeadsUpManager.isAlerting(group.summary.getKey())) { + alertNotificationWhenPossible(group.summary); } + groupAlertEntry.mAlertSummaryOnNextAddition = false; } else { - // Group summary can be null if we are no longer suppressed because the summary was - // removed. In that case, we don't need to alert the summary. - if (group.summary == null) { - return; - } - GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey( - group.summary.getSbn())); - // Group is no longer suppressed. We should check if we need to transfer the alert - // back to the summary now that it's no longer suppressed. - if (groupAlertEntry.mAlertSummaryOnNextAddition) { - if (!mHeadsUpManager.isAlerting(group.summary.getKey())) { - alertNotificationWhenPossible(group.summary, mHeadsUpManager); - } - groupAlertEntry.mAlertSummaryOnNextAddition = false; - } else { - checkShouldTransferBack(groupAlertEntry); - } + checkShouldTransferBack(groupAlertEntry); } } - }; + } @Override public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { - onAlertStateChanged(entry, isHeadsUp, mHeadsUpManager); + if (DEBUG) { + Log.d(TAG, "!! onHeadsUpStateChanged: entry=" + entry + " isHeadsUp=" + isHeadsUp); + } + if (isHeadsUp && entry.getSbn().getNotification().isGroupSummary()) { + // a group summary is alerting; trigger the forward transfer checks + checkForForwardAlertTransfer(entry, /* oldAlertOverride */ null); + } } - private void onAlertStateChanged(NotificationEntry entry, boolean isAlerting, - AlertingNotificationManager alertManager) { - if (isAlerting && mGroupManager.isSummaryOfSuppressedGroup(entry.getSbn())) { - handleSuppressedSummaryAlerted(entry, alertManager); + /** + * Handles changes in a group's suppression or alertOverride, but where at least one of those + * conditions is still true (either the group is suppressed, the group has an alertOverride, + * or both). The method determined which kind of child needs to receive the alert, finds the + * entry currently alerting, and makes the transfer. + * + * Internally, this is handled with two main cases: the override needs the alert, or there is + * no override but the summary is suppressed (so an isolated child needs the alert). + * + * @param summary the notification entry of the summary of the logical group. + * @param oldAlertOverride the former value of group.alertOverride, before whatever event + * required us to check for for a transfer condition. + */ + private void checkForForwardAlertTransfer(NotificationEntry summary, + NotificationEntry oldAlertOverride) { + if (DEBUG) { + Log.d(TAG, "checkForForwardAlertTransfer: enter"); + } + NotificationGroup group = mGroupManager.getGroupForSummary(summary.getSbn()); + if (group != null && group.alertOverride != null) { + handleOverriddenSummaryAlerted(summary); + } else if (mGroupManager.isSummaryOfSuppressedGroup(summary.getSbn())) { + handleSuppressedSummaryAlerted(summary, oldAlertOverride); } } @@ -186,9 +247,16 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis // see as early as we can if we need to abort a transfer. @Override public void onPendingEntryAdded(NotificationEntry entry) { + if (DEBUG) { + Log.d(TAG, "!! onPendingEntryAdded: entry=" + entry); + } String groupKey = mGroupManager.getGroupKey(entry.getSbn()); GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey); - if (groupAlertEntry != null) { + if (groupAlertEntry != null && groupAlertEntry.mGroup.alertOverride == null) { + // new pending group entries require us to transfer back from the child to the + // group, but alertOverrides are only present in very limited circumstances, so + // while it's possible the group should ALSO alert, the previous detection which set + // this alertOverride won't be invalidated by this notification added to this group. checkShouldTransferBack(groupAlertEntry); } } @@ -262,43 +330,128 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis } /** - * Handles the scenario where a summary that has been suppressed is alerted. A suppressed + * Handles the scenario where a summary that has been suppressed is itself, or has a former + * alertOverride (in the form of an isolated logical child) which was alerted. A suppressed * summary should for all intents and purposes be invisible to the user and as a result should * not alert. When this is the case, it is our responsibility to pass the alert to the * appropriate child which will be the representative notification alerting for the group. * - * @param summary the summary that is suppressed and alerting - * @param alertManager the alert manager that manages the alerting summary + * @param summary the summary that is suppressed and (potentially) alerting + * @param oldAlertOverride the alertOverride before whatever event triggered this method. If + * the alert override was removed, this will be the entry that should + * be transferred back from. */ private void handleSuppressedSummaryAlerted(@NonNull NotificationEntry summary, - @NonNull AlertingNotificationManager alertManager) { - StatusBarNotification sbn = summary.getSbn(); + NotificationEntry oldAlertOverride) { + if (DEBUG) { + Log.d(TAG, "handleSuppressedSummaryAlerted: summary=" + summary); + } GroupAlertEntry groupAlertEntry = - mGroupAlertEntries.get(mGroupManager.getGroupKey(sbn)); + mGroupAlertEntries.get(mGroupManager.getGroupKey(summary.getSbn())); + if (!mGroupManager.isSummaryOfSuppressedGroup(summary.getSbn()) - || !alertManager.isAlerting(sbn.getKey()) || groupAlertEntry == null) { + if (DEBUG) { + Log.d(TAG, "handleSuppressedSummaryAlerted: invalid state"); + } + return; + } + boolean summaryIsAlerting = mHeadsUpManager.isAlerting(summary.getKey()); + boolean priorityIsAlerting = oldAlertOverride != null + && mHeadsUpManager.isAlerting(oldAlertOverride.getKey()); + if (!summaryIsAlerting && !priorityIsAlerting) { + if (DEBUG) { + Log.d(TAG, "handleSuppressedSummaryAlerted: no summary or override alerting"); + } return; } if (pendingInflationsWillAddChildren(groupAlertEntry.mGroup)) { // New children will actually be added to this group, let's not transfer the alert. + if (DEBUG) { + Log.d(TAG, "handleSuppressedSummaryAlerted: pending inflations"); + } return; } NotificationEntry child = mGroupManager.getLogicalChildren(summary.getSbn()).iterator().next(); - if (child != null) { - if (child.getRow().keepInParent() - || child.isRowRemoved() - || child.isRowDismissed()) { - // The notification is actually already removed. No need to alert it. - return; + if (summaryIsAlerting) { + if (DEBUG) { + Log.d(TAG, "handleSuppressedSummaryAlerted: transfer summary -> child"); } - if (!alertManager.isAlerting(child.getKey()) && onlySummaryAlerts(summary)) { - groupAlertEntry.mLastAlertTransferTime = SystemClock.elapsedRealtime(); + tryTransferAlertState(summary, /*from*/ summary, /*to*/ child, groupAlertEntry); + return; + } + // Summary didn't have the alert, so we're in "transfer back" territory. First, make sure + // it's not too late to transfer back, then transfer the alert from the oldAlertOverride to + // the isolated child which should receive the alert. + if (!canStillTransferBack(groupAlertEntry)) { + if (DEBUG) { + Log.d(TAG, "handleSuppressedSummaryAlerted: transfer from override: too late"); + } + return; + } + + if (DEBUG) { + Log.d(TAG, "handleSuppressedSummaryAlerted: transfer override -> child"); + } + tryTransferAlertState(summary, /*from*/ oldAlertOverride, /*to*/ child, groupAlertEntry); + } + + /** + * Checks for and handles the scenario where the given entry is the summary of a group which + * has an alertOverride, and either the summary itself or one of its logical isolated children + * is currently alerting (which happens if the summary is suppressed). + */ + private void handleOverriddenSummaryAlerted(NotificationEntry summary) { + if (DEBUG) { + Log.d(TAG, "handleOverriddenSummaryAlerted: summary=" + summary); + } + GroupAlertEntry groupAlertEntry = + mGroupAlertEntries.get(mGroupManager.getGroupKey(summary.getSbn())); + NotificationGroup group = mGroupManager.getGroupForSummary(summary.getSbn()); + if (group == null || group.alertOverride == null || groupAlertEntry == null) { + if (DEBUG) { + Log.d(TAG, "handleOverriddenSummaryAlerted: invalid state"); + } + return; + } + boolean summaryIsAlerting = mHeadsUpManager.isAlerting(summary.getKey()); + if (summaryIsAlerting) { + if (DEBUG) { + Log.d(TAG, "handleOverriddenSummaryAlerted: transfer summary -> override"); + } + tryTransferAlertState(summary, /*from*/ summary, group.alertOverride, groupAlertEntry); + return; + } + // Summary didn't have the alert, so we're in "transfer back" territory. First, make sure + // it's not too late to transfer back, then remove the alert from any of the logical + // children, and if one of them was alerting, we can alert the override. + if (!canStillTransferBack(groupAlertEntry)) { + if (DEBUG) { + Log.d(TAG, "handleOverriddenSummaryAlerted: transfer from child: too late"); + } + return; + } + List<NotificationEntry> children = mGroupManager.getLogicalChildren(summary.getSbn()); + if (children == null) { + if (DEBUG) { + Log.d(TAG, "handleOverriddenSummaryAlerted: no children"); + } + return; + } + children.remove(group.alertOverride); // do not release the alert on our desired destination + boolean releasedChild = releaseChildAlerts(children); + if (releasedChild) { + if (DEBUG) { + Log.d(TAG, "handleOverriddenSummaryAlerted: transfer child -> override"); + } + tryTransferAlertState(summary, /*from*/ null, group.alertOverride, groupAlertEntry); + } else { + if (DEBUG) { + Log.d(TAG, "handleOverriddenSummaryAlerted: no child alert released"); } - transferAlertState(summary, child, alertManager); } } @@ -307,14 +460,37 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis * immediately to have the incorrect one up as short as possible. The second should alert * when possible. * + * @param summary entry of the summary * @param fromEntry entry to transfer alert from * @param toEntry entry to transfer to - * @param alertManager alert manager for the alert type */ - private void transferAlertState(@NonNull NotificationEntry fromEntry, @NonNull NotificationEntry toEntry, - @NonNull AlertingNotificationManager alertManager) { - alertManager.removeNotification(fromEntry.getKey(), true /* releaseImmediately */); - alertNotificationWhenPossible(toEntry, alertManager); + private void tryTransferAlertState( + NotificationEntry summary, + NotificationEntry fromEntry, + NotificationEntry toEntry, + GroupAlertEntry groupAlertEntry) { + if (toEntry != null) { + if (toEntry.getRow().keepInParent() + || toEntry.isRowRemoved() + || toEntry.isRowDismissed()) { + // The notification is actually already removed. No need to alert it. + return; + } + if (!mHeadsUpManager.isAlerting(toEntry.getKey()) && onlySummaryAlerts(summary)) { + groupAlertEntry.mLastAlertTransferTime = SystemClock.elapsedRealtime(); + } + if (DEBUG) { + Log.d(TAG, "transferAlertState: fromEntry=" + fromEntry + " toEntry=" + toEntry); + } + transferAlertState(fromEntry, toEntry); + } + } + private void transferAlertState(@Nullable NotificationEntry fromEntry, + @NonNull NotificationEntry toEntry) { + if (fromEntry != null) { + mHeadsUpManager.removeNotification(fromEntry.getKey(), true /* releaseImmediately */); + } + alertNotificationWhenPossible(toEntry); } /** @@ -326,11 +502,13 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis * more children are coming. Thus, if a child is added within a certain timeframe after we * transfer, we back out and alert the summary again. * + * An alert can only transfer back within a small window of time after a transfer away from the + * summary to a child happened. + * * @param groupAlertEntry group alert entry to check */ private void checkShouldTransferBack(@NonNull GroupAlertEntry groupAlertEntry) { - if (SystemClock.elapsedRealtime() - groupAlertEntry.mLastAlertTransferTime - < ALERT_TRANSFER_TIMEOUT) { + if (canStillTransferBack(groupAlertEntry)) { NotificationEntry summary = groupAlertEntry.mGroup.summary; if (!onlySummaryAlerts(summary)) { @@ -338,30 +516,17 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis } ArrayList<NotificationEntry> children = mGroupManager.getLogicalChildren( summary.getSbn()); - int numChildren = children.size(); + int numActiveChildren = children.size(); int numPendingChildren = getPendingChildrenNotAlerting(groupAlertEntry.mGroup); - numChildren += numPendingChildren; + int numChildren = numActiveChildren + numPendingChildren; if (numChildren <= 1) { return; } - boolean releasedChild = false; - for (int i = 0; i < children.size(); i++) { - NotificationEntry entry = children.get(i); - if (onlySummaryAlerts(entry) && mHeadsUpManager.isAlerting(entry.getKey())) { - releasedChild = true; - mHeadsUpManager.removeNotification( - entry.getKey(), true /* releaseImmediately */); - } - if (mPendingAlerts.containsKey(entry.getKey())) { - // This is the child that would've been removed if it was inflated. - releasedChild = true; - mPendingAlerts.get(entry.getKey()).mAbortOnInflation = true; - } - } + boolean releasedChild = releaseChildAlerts(children); if (releasedChild && !mHeadsUpManager.isAlerting(summary.getKey())) { - boolean notifyImmediately = (numChildren - numPendingChildren) > 1; + boolean notifyImmediately = numActiveChildren > 1; if (notifyImmediately) { - alertNotificationWhenPossible(summary, mHeadsUpManager); + alertNotificationWhenPossible(summary); } else { // Should wait until the pending child inflates before alerting. groupAlertEntry.mAlertSummaryOnNextAddition = true; @@ -371,25 +536,61 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis } } + private boolean canStillTransferBack(@NonNull GroupAlertEntry groupAlertEntry) { + return SystemClock.elapsedRealtime() - groupAlertEntry.mLastAlertTransferTime + < ALERT_TRANSFER_TIMEOUT; + } + + private boolean releaseChildAlerts(List<NotificationEntry> children) { + boolean releasedChild = false; + if (SPEW) { + Log.d(TAG, "releaseChildAlerts: numChildren=" + children.size()); + } + for (int i = 0; i < children.size(); i++) { + NotificationEntry entry = children.get(i); + if (SPEW) { + Log.d(TAG, "releaseChildAlerts: checking i=" + i + " entry=" + entry + + " onlySummaryAlerts=" + onlySummaryAlerts(entry) + + " isAlerting=" + mHeadsUpManager.isAlerting(entry.getKey()) + + " isPendingAlert=" + mPendingAlerts.containsKey(entry.getKey())); + } + if (onlySummaryAlerts(entry) && mHeadsUpManager.isAlerting(entry.getKey())) { + releasedChild = true; + mHeadsUpManager.removeNotification( + entry.getKey(), true /* releaseImmediately */); + } + if (mPendingAlerts.containsKey(entry.getKey())) { + // This is the child that would've been removed if it was inflated. + releasedChild = true; + mPendingAlerts.get(entry.getKey()).mAbortOnInflation = true; + } + } + if (SPEW) { + Log.d(TAG, "releaseChildAlerts: didRelease=" + releasedChild); + } + return releasedChild; + } + /** * Tries to alert the notification. If its content view is not inflated, we inflate and continue * when the entry finishes inflating the view. * * @param entry entry to show - * @param alertManager alert manager for the alert type */ - private void alertNotificationWhenPossible(@NonNull NotificationEntry entry, - @NonNull AlertingNotificationManager alertManager) { - @InflationFlag int contentFlag = alertManager.getContentFlag(); + private void alertNotificationWhenPossible(@NonNull NotificationEntry entry) { + @InflationFlag int contentFlag = mHeadsUpManager.getContentFlag(); final RowContentBindParams params = mRowContentBindStage.getStageParams(entry); if ((params.getContentViews() & contentFlag) == 0) { + if (DEBUG) { + Log.d(TAG, "alertNotificationWhenPossible: async requestRebind entry=" + entry); + } mPendingAlerts.put(entry.getKey(), new PendingAlertInfo(entry)); params.requireContentViews(contentFlag); mRowContentBindStage.requestRebind(entry, en -> { PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.getKey()); if (alertInfo != null) { if (alertInfo.isStillValid()) { - alertNotificationWhenPossible(entry, mHeadsUpManager); + alertNotificationWhenPossible(entry); } else { // The transfer is no longer valid. Free the content. mRowContentBindStage.getStageParams(entry).markContentViewsFreeable( @@ -400,10 +601,16 @@ public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedLis }); return; } - if (alertManager.isAlerting(entry.getKey())) { - alertManager.updateNotification(entry.getKey(), true /* alert */); + if (mHeadsUpManager.isAlerting(entry.getKey())) { + if (DEBUG) { + Log.d(TAG, "alertNotificationWhenPossible: continue alerting entry=" + entry); + } + mHeadsUpManager.updateNotification(entry.getKey(), true /* alert */); } else { - alertManager.showNotification(entry); + if (DEBUG) { + Log.d(TAG, "alertNotificationWhenPossible: start alerting entry=" + entry); + } + mHeadsUpManager.showNotification(entry); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 137b7226f137..6ef4663ea5f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -1412,7 +1412,8 @@ public class StatusBar extends SystemUI implements DemoMode, mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider( mNotificationShadeWindowViewController, mStackScrollerController.getNotificationListContainer(), - mNotificationShadeDepthControllerLazy.get() + mNotificationShadeDepthControllerLazy.get(), + mHeadsUpManager ); // TODO: inject this. @@ -2785,7 +2786,7 @@ public class StatusBar extends SystemUI implements DemoMode, intent, mLockscreenUserManager.getCurrentUserId()); ActivityLaunchAnimator.Controller animController = null; - if (animationController != null && areLaunchAnimationsEnabled()) { + if (animationController != null) { animController = dismissShade ? new StatusBarLaunchAnimatorController( animationController, this, true /* isLaunchForActivity */) : animationController; @@ -2801,46 +2802,48 @@ public class StatusBar extends SystemUI implements DemoMode, intent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.addFlags(flags); - int[] result = new int[] { ActivityManager.START_CANCELED }; - - mActivityLaunchAnimator.startIntentWithAnimation(animCallbackForLambda, (adapter) -> { - ActivityOptions options = new ActivityOptions( - getActivityOptions(mDisplayId, adapter)); - options.setDisallowEnterPictureInPictureWhileLaunching( - disallowEnterPictureInPictureWhileLaunching); - if (CameraIntents.isInsecureCameraIntent(intent)) { - // Normally an activity will set it's requested rotation - // animation on its window. However when launching an activity - // causes the orientation to change this is too late. In these cases - // the default animation is used. This doesn't look good for - // the camera (as it rotates the camera contents out of sync - // with physical reality). So, we ask the WindowManager to - // force the crossfade animation if an orientation change - // happens to occur during the launch. - options.setRotationAnimationHint( - WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS); - } - if (intent.getAction() == Settings.Panel.ACTION_VOLUME) { - // Settings Panel is implemented as activity(not a dialog), so - // underlying app is paused and may enter picture-in-picture mode - // as a result. - // So we need to disable picture-in-picture mode here - // if it is volume panel. - options.setDisallowEnterPictureInPictureWhileLaunching(true); - } + int[] result = new int[]{ActivityManager.START_CANCELED}; + + mActivityLaunchAnimator.startIntentWithAnimation(animCallbackForLambda, + areLaunchAnimationsEnabled(), (adapter) -> { + ActivityOptions options = new ActivityOptions( + getActivityOptions(mDisplayId, adapter)); + options.setDisallowEnterPictureInPictureWhileLaunching( + disallowEnterPictureInPictureWhileLaunching); + if (CameraIntents.isInsecureCameraIntent(intent)) { + // Normally an activity will set it's requested rotation + // animation on its window. However when launching an activity + // causes the orientation to change this is too late. In these cases + // the default animation is used. This doesn't look good for + // the camera (as it rotates the camera contents out of sync + // with physical reality). So, we ask the WindowManager to + // force the crossfade animation if an orientation change + // happens to occur during the launch. + options.setRotationAnimationHint( + WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS); + } + if (intent.getAction() == Settings.Panel.ACTION_VOLUME) { + // Settings Panel is implemented as activity(not a dialog), so + // underlying app is paused and may enter picture-in-picture mode + // as a result. + // So we need to disable picture-in-picture mode here + // if it is volume panel. + options.setDisallowEnterPictureInPictureWhileLaunching(true); + } - try { - result[0] = ActivityTaskManager.getService().startActivityAsUser( - null, mContext.getBasePackageName(), mContext.getAttributionTag(), - intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, - options.toBundle(), UserHandle.CURRENT.getIdentifier()); - } catch (RemoteException e) { - Log.w(TAG, "Unable to start activity", e); - } - return result[0]; - }); + try { + result[0] = ActivityTaskManager.getService().startActivityAsUser( + null, mContext.getBasePackageName(), + mContext.getAttributionTag(), + intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, + options.toBundle(), UserHandle.CURRENT.getIdentifier()); + } catch (RemoteException e) { + Log.w(TAG, "Unable to start activity", e); + } + return result[0]; + }); if (callback != null) { callback.onActivityStarted(result[0]); @@ -4559,19 +4562,17 @@ public class StatusBar extends SystemUI implements DemoMode, && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(), mLockscreenUserManager.getCurrentUserId()); - boolean animate = animationController != null && areLaunchAnimationsEnabled(); - boolean collapse = !animate; + boolean collapse = animationController == null; executeActionDismissingKeyguard(() -> { try { // We wrap animationCallback with a StatusBarLaunchAnimatorController so that the // shade is collapsed after the animation (or when it is cancelled, aborted, etc). ActivityLaunchAnimator.Controller controller = - animate ? new StatusBarLaunchAnimatorController(animationController, this, - intent.isActivity()) - : null; + animationController != null ? new StatusBarLaunchAnimatorController( + animationController, this, intent.isActivity()) : null; mActivityLaunchAnimator.startPendingIntentWithAnimation( - controller, + controller, areLaunchAnimationsEnabled(), (animationAdapter) -> intent.sendAndReturnResult(null, 0, null, null, null, null, getActivityOptions(mDisplayId, animationAdapter))); } catch (PendingIntent.CanceledException e) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 4356b52d27ea..ab58aae6857e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -283,7 +283,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mLogger.logHandleClickAfterKeyguardDismissed(entry.getKey()); // TODO: Some of this code may be able to move to NotificationEntryManager. - removeHUN(row); + String key = row.getEntry().getSbn().getKey(); + if (mHeadsUpManager != null && mHeadsUpManager.isAlerting(key)) { + // Release the HUN notification to the shade. + if (mPresenter.isPresenterFullyCollapsed()) { + HeadsUpUtil.setIsClickedHeadsUpNotification(row, true); + } + } final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed( entry, row, controller, intent, @@ -331,6 +337,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit // bypass work challenge if (mStatusBarRemoteInputCallback.startWorkChallengeIfNecessary(userId, intent.getIntentSender(), notificationKey)) { + removeHUN(row); // Show work challenge, do not run PendingIntent and // remove notification collapseOnMainThread(); @@ -350,6 +357,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit final boolean canBubble = entry.canBubble(); if (canBubble) { mLogger.logExpandingBubble(notificationKey); + removeHUN(row); expandBubbleStackOnMainThread(entry); } else { startNotificationIntent( @@ -422,14 +430,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean isActivityIntent) { mLogger.logStartNotificationIntent(entry.getKey(), intent); try { - ActivityLaunchAnimator.Controller animationController = null; - if (!wasOccluded && mStatusBar.areLaunchAnimationsEnabled()) { - animationController = new StatusBarLaunchAnimatorController( - mNotificationAnimationProvider.getAnimatorController(row), mStatusBar, - isActivityIntent); - } + ActivityLaunchAnimator.Controller animationController = + new StatusBarLaunchAnimatorController( + mNotificationAnimationProvider.getAnimatorController(row), mStatusBar, + isActivityIntent); mActivityLaunchAnimator.startPendingIntentWithAnimation(animationController, + !wasOccluded && mStatusBar.areLaunchAnimationsEnabled(), (adapter) -> { long eventTime = row.getAndResetLastActionUpTime(); Bundle options = eventTime > 0 @@ -442,13 +449,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit return intent.sendAndReturnResult(mContext, 0, fillInIntent, null, null, null, options); }); - - // Note that other cases when we should still collapse (like activity already on top) is - // handled by the StatusBarLaunchAnimatorController. - boolean shouldCollapse = animationController == null; - if (shouldCollapse) { - collapseOnMainThread(); - } } catch (PendingIntent.CanceledException e) { // the stack trace isn't very helpful here. // Just log the exception message. @@ -462,34 +462,19 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit ExpandableNotificationRow row) { mActivityStarter.dismissKeyguardThenExecute(() -> { AsyncTask.execute(() -> { - ActivityLaunchAnimator.Controller animationController = null; - if (mStatusBar.areLaunchAnimationsEnabled()) { - animationController = new StatusBarLaunchAnimatorController( - mNotificationAnimationProvider.getAnimatorController(row), mStatusBar, - true /* isActivityIntent */); - } + ActivityLaunchAnimator.Controller animationController = + new StatusBarLaunchAnimatorController( + mNotificationAnimationProvider.getAnimatorController(row), + mStatusBar, true /* isActivityIntent */); mActivityLaunchAnimator.startIntentWithAnimation( - animationController, + animationController, mStatusBar.areLaunchAnimationsEnabled(), (adapter) -> TaskStackBuilder.create(mContext) .addNextIntentWithParentStack(intent) .startActivities(getActivityOptions( mStatusBar.getDisplayId(), adapter), new UserHandle(UserHandle.getUserId(appUid)))); - - // Note that other cases when we should still collapse (like activity already on - // top) is handled by the StatusBarLaunchAnimatorController. - boolean shouldCollapse = animationController == null; - - // Putting it back on the main thread, since we're touching views - mMainThreadHandler.post(() -> { - removeHUN(row); - if (shouldCollapse) { - mCommandQueue.animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, - true /* force */); - } - }); }); return true; }, null, false /* afterKeyguardGone */); @@ -508,26 +493,16 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit tsb.addNextIntent(intent); } - ActivityLaunchAnimator.Controller animationController = null; - if (mStatusBar.areLaunchAnimationsEnabled()) { - animationController = new StatusBarLaunchAnimatorController( - ActivityLaunchAnimator.Controller.fromView(view), mStatusBar, - true /* isActivityIntent */); - } + ActivityLaunchAnimator.Controller animationController = + new StatusBarLaunchAnimatorController( + ActivityLaunchAnimator.Controller.fromView(view), mStatusBar, + true /* isActivityIntent */); mActivityLaunchAnimator.startIntentWithAnimation(animationController, + mStatusBar.areLaunchAnimationsEnabled(), (adapter) -> tsb.startActivities( getActivityOptions(mStatusBar.getDisplayId(), adapter), UserHandle.CURRENT)); - - // Note that other cases when we should still collapse (like activity already on - // top) is handled by the StatusBarLaunchAnimatorController. - boolean shouldCollapse = animationController == null; - if (shouldCollapse) { - // Putting it back on the main thread, since we're touching views - mMainThreadHandler.post(() -> mCommandQueue.animateCollapsePanels( - CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */)); - } }); return true; }, null, false /* afterKeyguardGone */); @@ -536,11 +511,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private void removeHUN(ExpandableNotificationRow row) { String key = row.getEntry().getSbn().getKey(); if (mHeadsUpManager != null && mHeadsUpManager.isAlerting(key)) { - // Release the HUN notification to the shade. - if (mPresenter.isPresenterFullyCollapsed()) { - HeadsUpUtil.setIsClickedHeadsUpNotification(row, true); - } - // In most cases, when FLAG_AUTO_CANCEL is set, the notification will // become canceled shortly by NoMan, but we can't assume that. mHeadsUpManager.removeNotification(key, true /* releaseImmediately */); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index 0bf2d503e5a0..e3e2572b9b4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -341,7 +341,6 @@ interface SmartActionInflater { activityStarter.startPendingIntentDismissingKeyguard(action.actionIntent, entry.row) { smartReplyController .smartActionClicked(entry, actionIndex, action, smartActions.fromAssistant) - headsUpManager.removeNotification(entry.key, true /* releaseImmediately */) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapterTest.java index 899625eee7d9..afd5f77f7a4c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityTargetAdapterTest.java @@ -65,9 +65,9 @@ public class AccessibilityTargetAdapterTest extends SysuiTestCase { mTargets.add(mAccessibilityTarget); mAdapter = new AccessibilityTargetAdapter(mTargets); - final View root = LayoutInflater.from(mContext).inflate( + final View rootView = LayoutInflater.from(mContext).inflate( R.layout.accessibility_floating_menu_item, null); - mViewHolder = new ViewHolder(root); + mViewHolder = new ViewHolder(rootView); when(mAccessibilityTarget.getIcon()).thenReturn(mIcon); when(mIcon.getConstantState()).thenReturn(mConstantState); } @@ -82,4 +82,27 @@ public class AccessibilityTargetAdapterTest extends SysuiTestCase { assertThat(actualIconWith).isEqualTo(iconWidthHeight); } + + @Test + public void getContentDescription_invisibleToggleTarget_descriptionWithoutState() { + when(mAccessibilityTarget.getFragmentType()).thenReturn(/* InvisibleToggle */ 1); + when(mAccessibilityTarget.getLabel()).thenReturn("testLabel"); + when(mAccessibilityTarget.getStateDescription()).thenReturn("testState"); + + mAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.itemView.getContentDescription().toString().contentEquals( + "testLabel")).isTrue(); + } + + @Test + public void getStateDescription_toggleTarget_switchOff_stateOffText() { + when(mAccessibilityTarget.getFragmentType()).thenReturn(/* Toggle */ 2); + when(mAccessibilityTarget.getStateDescription()).thenReturn("testState"); + + mAdapter.onBindViewHolder(mViewHolder, 0); + + assertThat(mViewHolder.itemView.getStateDescription().toString().contentEquals( + "testState")).isTrue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index c023610b6052..fbba09a255e7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -44,12 +44,13 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { private fun startIntentWithAnimation( controller: ActivityLaunchAnimator.Controller? = this.controller, + animate: Boolean = true, intentStarter: (RemoteAnimationAdapter?) -> Int ) { // We start in a new thread so that we can ensure that the callbacks are called in the main // thread. thread { - activityLaunchAnimator.startIntentWithAnimation(controller, intentStarter) + activityLaunchAnimator.startIntentWithAnimation(controller, animate, intentStarter) }.join() } @@ -95,6 +96,16 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() { } @Test + fun doesNotAnimateIfAnimateIsFalse() { + val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java) + startIntentWithAnimation(animate = false) { ActivityManager.START_SUCCESS } + + waitForIdleSync() + verify(controller).onIntentStarted(willAnimateCaptor.capture()) + assertFalse(willAnimateCaptor.value) + } + + @Test fun doesNotStartIfAnimationIsCancelled() { val runner = activityLaunchAnimator.createRunner(controller) runner.onAnimationCancelled() diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java index 47f41836f1a6..b85af4846390 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarRotationContextTest.java @@ -62,8 +62,8 @@ public class NavigationBarRotationContextTest extends SysuiTestCase { final View view = new View(mContext); mRotationButton = mock(RotationButton.class); - mRotationButtonController = spy(new RotationButtonController(mContext, 0, 0, - mRotationButton, (visibility) -> {})); + mRotationButtonController = spy(new RotationButtonController(mContext, 0, 0)); + mRotationButtonController.setRotationButton(mRotationButton, (visibility) -> {}); final KeyButtonDrawable kbd = mock(KeyButtonDrawable.class); doReturn(view).when(mRotationButton).getCurrentView(); doReturn(true).when(mRotationButton).acceptRotationProposal(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleTileViewHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleTileViewHelperTest.java index d353d529865b..764cdee7e36d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleTileViewHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleTileViewHelperTest.java @@ -76,6 +76,14 @@ public class PeopleTileViewHelperTest extends SysuiTestCase { private static final String NAME = "username"; private static final UserHandle USER = new UserHandle(0); private static final String SENDER = "sender"; + + private static final CharSequence EMOJI_BR_FLAG = "\ud83c\udde7\ud83c\uddf7"; + private static final CharSequence EMOJI_BEAR = "\ud83d\udc3b"; + private static final CharSequence EMOJI_THUMBS_UP_BROWN_SKIN = "\uD83D\uDC4D\uD83C\uDFFD"; + private static final CharSequence EMOJI_JOY = "\uD83D\uDE02"; + private static final CharSequence EMOJI_FAMILY = + "\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc67"; + private static final PeopleSpaceTile PERSON_TILE_WITHOUT_NOTIFICATION = new PeopleSpaceTile .Builder(SHORTCUT_ID_1, NAME, ICON, new Intent()) @@ -701,94 +709,151 @@ public class PeopleTileViewHelperTest extends SysuiTestCase { @Test - public void testGetBackgroundTextFromMessageNoPunctuation() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test"); + public void testGetDoublePunctuationNoPunctuation() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation("test"); assertThat(backgroundText).isNull(); } @Test - public void testGetBackgroundTextFromMessageSingleExclamation() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test!"); + public void testGetDoublePunctuationSingleExclamation() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation("test!"); assertThat(backgroundText).isNull(); } @Test - public void testGetBackgroundTextFromMessageSingleQuestion() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("?test"); + public void testGetDoublePunctuationSingleQuestion() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation("?test"); assertThat(backgroundText).isNull(); } @Test - public void testGetBackgroundTextFromMessageSeparatedMarks() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test! right!"); + public void testGetDoublePunctuationSeparatedMarks() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation("test! right!"); assertThat(backgroundText).isNull(); } @Test - public void testGetBackgroundTextFromMessageDoubleExclamation() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("!!test"); + public void testGetDoublePunctuationDoubleExclamation() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation("!!test"); assertThat(backgroundText).isEqualTo("!"); } @Test - public void testGetBackgroundTextFromMessageDoubleQuestion() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test??"); + public void testGetDoublePunctuationDoubleQuestion() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation("test??"); assertThat(backgroundText).isEqualTo("?"); } @Test - public void testGetBackgroundTextFromMessageMixed() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage("test?!"); + public void testGetDoublePunctuationMixed() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation("test?!"); assertThat(backgroundText).isEqualTo("!?"); } @Test - public void testGetBackgroundTextFromMessageMixedInTheMiddle() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + public void testGetDoublePunctuationMixedInTheMiddle() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation( "test!? in the middle"); assertThat(backgroundText).isEqualTo("!?"); } @Test - public void testGetBackgroundTextFromMessageMixedDifferentOrder() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + public void testGetDoublePunctuationMixedDifferentOrder() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation( "test!? in the middle"); assertThat(backgroundText).isEqualTo("!?"); } @Test - public void testGetBackgroundTextFromMessageMultiple() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + public void testGetDoublePunctuationMultiple() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation( "test!?!!? in the middle"); assertThat(backgroundText).isEqualTo("!?"); } @Test - public void testGetBackgroundTextFromMessageQuestionFirst() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + public void testGetDoublePunctuationQuestionFirst() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation( "test?? in the middle!!"); assertThat(backgroundText).isEqualTo("?"); } @Test - public void testGetBackgroundTextFromMessageExclamationFirst() { - String backgroundText = mPeopleTileViewHelper.getBackgroundTextFromMessage( + public void testGetDoublePunctuationExclamationFirst() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoublePunctuation( "test!! in the middle??"); assertThat(backgroundText).isEqualTo("!"); } + @Test + public void testGetDoubleEmojisNoEmojis() { + CharSequence backgroundText = mPeopleTileViewHelper + .getDoubleEmoji("This string has no emojis."); + assertThat(backgroundText).isNull(); + } + + @Test + public void testGetDoubleEmojisSingleEmoji() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoubleEmoji( + "This string has one emoji " + EMOJI_JOY + " in the middle."); + assertThat(backgroundText).isNull(); + } + + @Test + public void testGetDoubleEmojisSingleEmojiThenTwoEmojis() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoubleEmoji( + "This string has one emoji " + EMOJI_JOY + " in the middle, then two " + + EMOJI_BEAR + EMOJI_BEAR); + assertEquals(backgroundText, EMOJI_BEAR); + } + + @Test + public void testGetDoubleEmojisTwoEmojisWithModifier() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoubleEmoji( + "Yes! " + EMOJI_THUMBS_UP_BROWN_SKIN + EMOJI_THUMBS_UP_BROWN_SKIN + " Sure."); + assertEquals(backgroundText, EMOJI_THUMBS_UP_BROWN_SKIN); + } + + @Test + public void testGetDoubleEmojisTwoFlagEmojis() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoubleEmoji( + "Let's travel to " + EMOJI_BR_FLAG + EMOJI_BR_FLAG + " next year."); + assertEquals(backgroundText, EMOJI_BR_FLAG); + } + + @Test + public void testGetDoubleEmojiTwoBears() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoubleEmoji( + EMOJI_BEAR.toString() + EMOJI_BEAR.toString() + "bears!"); + assertEquals(backgroundText, EMOJI_BEAR); + } + + @Test + public void testGetDoubleEmojiTwoEmojisTwice() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoubleEmoji( + "Two sets of two emojis: " + EMOJI_FAMILY + EMOJI_FAMILY + EMOJI_BEAR + EMOJI_BEAR); + assertEquals(backgroundText, EMOJI_FAMILY); + } + + @Test + public void testGetDoubleEmojiTwoEmojisSeparated() { + CharSequence backgroundText = mPeopleTileViewHelper.getDoubleEmoji( + "Two emojis " + EMOJI_BEAR + " separated " + EMOJI_BEAR + "."); + assertThat(backgroundText).isNull(); + } + private int getSizeInDp(int dimenResourceId) { return (int) (mContext.getResources().getDimension(dimenResourceId) / mContext.getResources().getDisplayMetrics().density); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt index af75f2c66195..3a3d1546984d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt @@ -67,6 +67,7 @@ class UserDetailViewAdapterTest : SysuiTestCase() { mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater) `when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean())) .thenReturn(mInflatedUserDetailItemView) + `when`(mParent.context).thenReturn(mContext) adapter = UserDetailView.Adapter(mContext, mUserSwitcherController, uiEventLogger, falsingManagerFake) mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java index 919ddcb488c2..0772c03d10d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java @@ -315,8 +315,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { @Test public void testNoViewsFirstOrLastInSectionWhenSecondSectionEmpty() { - Assert.assertFalse(mFirst.isFirstInSection()); - Assert.assertFalse(mFirst.isLastInSection()); + Assert.assertTrue(mFirst.isFirstInSection()); + Assert.assertTrue(mFirst.isLastInSection()); } @Test @@ -325,8 +325,8 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(null, null), createSection(mSecond, mSecond) }); - Assert.assertFalse(mSecond.isFirstInSection()); - Assert.assertFalse(mSecond.isLastInSection()); + Assert.assertTrue(mSecond.isFirstInSection()); + Assert.assertTrue(mSecond.isLastInSection()); } @Test @@ -335,10 +335,10 @@ public class NotificationRoundnessManagerTest extends SysuiTestCase { createSection(mFirst, mFirst), createSection(mSecond, mSecond) }); - Assert.assertFalse(mFirst.isFirstInSection()); + Assert.assertTrue(mFirst.isFirstInSection()); Assert.assertTrue(mFirst.isLastInSection()); Assert.assertTrue(mSecond.isFirstInSection()); - Assert.assertFalse(mSecond.isLastInSection()); + Assert.assertTrue(mSecond.isLastInSection()); } private NotificationSection createSection(ExpandableNotificationRow first, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 5170168c174c..be86af585fc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -61,6 +61,7 @@ import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; +import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationActivityStarter; @@ -73,6 +74,7 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; +import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; @@ -184,6 +186,14 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { when(mOnUserInteractionCallback.getGroupSummaryToDismiss(mNotificationRow.getEntry())) .thenReturn(null); + HeadsUpManagerPhone headsUpManager = mock(HeadsUpManagerPhone.class); + NotificationLaunchAnimatorControllerProvider notificationAnimationProvider = + new NotificationLaunchAnimatorControllerProvider( + mock(NotificationShadeWindowViewController.class), mock( + NotificationListContainer.class), + mock(NotificationShadeDepthController.class), + headsUpManager); + mNotificationActivityStarter = new StatusBarNotificationActivityStarter.Builder( getContext(), @@ -192,7 +202,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mUiBgExecutor, mEntryManager, mNotifPipeline, - mock(HeadsUpManagerPhone.class), + headsUpManager, mActivityStarter, mClickNotifier, mock(StatusBarStateController.class), @@ -220,8 +230,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { .setNotificationPanelViewController( mock(NotificationPanelViewController.class)) .setActivityLaunchAnimator(mActivityLaunchAnimator) - .setNotificationAnimatorControllerProvider( - mock(NotificationLaunchAnimatorControllerProvider.class)) + .setNotificationAnimatorControllerProvider(notificationAnimationProvider) .build(); // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg @@ -259,7 +268,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { // Then verify(mShadeController, atLeastOnce()).collapsePanel(); - verify(mActivityLaunchAnimator).startPendingIntentWithAnimation(eq(null), any()); + verify(mActivityLaunchAnimator).startPendingIntentWithAnimation(any(), + eq(false) /* animate */, any()); verify(mAssistManager).hideAssist(); diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 0c785da5c242..a1a4418b8d17 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -1850,7 +1850,7 @@ class StorageManagerService extends IStorageManager.Stub public StorageManagerService(Context context) { sSelf = this; mVoldAppDataIsolationEnabled = SystemProperties.getBoolean( - ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, false); + ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, true); mContext = context; mResolver = mContext.getContentResolver(); mCallbacks = new Callbacks(FgThread.get().getLooper()); diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index d8af01eb7bda..ab3060ae4bce 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -1051,9 +1051,9 @@ final class UiModeManagerService extends SystemService { } private static void assertSingleProjectionType(@UiModeManager.ProjectionType int p) { - // To be a single projection type it must be greater than zero and an exact power of two. + // To be a single projection type it must be non-zero and an exact power of two. boolean projectionTypeIsPowerOfTwoOrZero = (p & p - 1) == 0; - if (p <= 0 || !projectionTypeIsPowerOfTwoOrZero) { + if (p == 0 || !projectionTypeIsPowerOfTwoOrZero) { throw new IllegalArgumentException("Must specify exactly one projection type."); } } diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index bf574521b895..d8eccef8488e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -16,6 +16,9 @@ package com.android.server.am; +import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED; +import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_NONE; + import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_POWER_QUICK; import android.app.ActivityThread; @@ -27,6 +30,7 @@ import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Message; +import android.os.PowerExemptionManager; import android.provider.DeviceConfig; import android.provider.DeviceConfig.OnPropertiesChangedListener; import android.provider.DeviceConfig.Properties; @@ -139,6 +143,11 @@ final class ActivityManagerConstants extends ContentObserver { private static final long DEFAULT_FG_TO_BG_FGS_GRACE_DURATION = 5 * 1000; private static final int DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS = 10 * 1000; private static final float DEFAULT_FGS_ATOM_SAMPLE_RATE = 1; // 100 % + /** + * Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED} + */ + private static final int + DEFAULT_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR = 1; // Flag stored in the DeviceConfig API. /** @@ -210,6 +219,13 @@ final class ActivityManagerConstants extends ContentObserver { private static final String KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME = "deferred_fgs_notification_exclusion_time"; + /** + * Default value for mPushMessagingOverQuotaBehavior if not explicitly set in + * Settings.Global. + */ + private static final String KEY_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR = + "push_messaging_over_quota_behavior"; + // Maximum number of cached processes we will allow. public int MAX_CACHED_PROCESSES = DEFAULT_MAX_CACHED_PROCESSES; @@ -413,6 +429,13 @@ final class ActivityManagerConstants extends ContentObserver { // before another FGS notifiction from that app can be deferred. volatile long mFgsNotificationDeferralExclusionTime = 2 * 60 * 1000L; + /** + * When server pushing message is over the quote, select one of the temp allow list type as + * defined in {@link PowerExemptionManager.TempAllowListType} + */ + volatile @PowerExemptionManager.TempAllowListType int mPushMessagingOverQuotaBehavior = + DEFAULT_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR; + /* * At boot time, broadcast receiver ACTION_BOOT_COMPLETED, ACTION_LOCKED_BOOT_COMPLETED and * ACTION_PRE_BOOT_COMPLETED are temp allowlisted to start FGS for a duration of time in @@ -605,6 +628,9 @@ final class ActivityManagerConstants extends ContentObserver { case KEY_DEFERRED_FGS_NOTIFICATION_EXCLUSION_TIME: updateFgsNotificationDeferralExclusionTime(); break; + case KEY_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR: + updatePushMessagingOverQuotaBehavior(); + break; case KEY_OOMADJ_UPDATE_POLICY: updateOomAdjUpdatePolicy(); break; @@ -909,6 +935,19 @@ final class ActivityManagerConstants extends ContentObserver { /*default value*/ 2 * 60 * 1000L); } + private void updatePushMessagingOverQuotaBehavior() { + mPushMessagingOverQuotaBehavior = DeviceConfig.getInt( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR, + DEFAULT_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR); + if (mPushMessagingOverQuotaBehavior < TEMPORARY_ALLOW_LIST_TYPE_NONE + || mPushMessagingOverQuotaBehavior + > TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED) { + mPushMessagingOverQuotaBehavior = + DEFAULT_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR; + } + } + private void updateOomAdjUpdatePolicy() { OOMADJ_UPDATE_QUICK = DeviceConfig.getInt( DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, @@ -1166,6 +1205,8 @@ final class ActivityManagerConstants extends ContentObserver { pw.print("="); pw.println(mFgsStartRestrictionCheckCallerTargetSdk); pw.print(" "); pw.print(KEY_FGS_ATOM_SAMPLE_RATE); pw.print("="); pw.println(mDefaultFgsAtomSampleRate); + pw.print(" "); pw.print(KEY_PUSH_MESSAGING_OVER_QUOTA_BEHAVIOR); + pw.print("="); pw.println(mPushMessagingOverQuotaBehavior); pw.println(); if (mOverrideMaxCachedProcesses >= 0) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 00b13b1bb6b0..9aedf1504df5 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -51,7 +51,8 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.IServiceManager.DUMP_FLAG_PROTO; import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS; import static android.os.PowerExemptionManager.REASON_SYSTEM_ALLOW_LISTED; -import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; +import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; +import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_NONE; import static android.os.Process.BLUETOOTH_UID; import static android.os.Process.FIRST_APPLICATION_UID; import static android.os.Process.INVALID_UID; @@ -14603,15 +14604,20 @@ public class ActivityManagerService extends IActivityManager.Stub */ @GuardedBy("this") void tempAllowlistUidLocked(int targetUid, long duration, @ReasonCode int reasonCode, - String reason, int type, int callingUid) { + String reason, @TempAllowListType int type, int callingUid) { synchronized (mProcLock) { + // The temp allowlist type could change according to the reasonCode. + type = mLocalDeviceIdleController.getTempAllowListType(reasonCode, type); + if (type == TEMPORARY_ALLOW_LIST_TYPE_NONE) { + return; + } mPendingTempAllowlist.put(targetUid, new PendingTempAllowlist(targetUid, duration, reasonCode, reason, type, callingUid)); setUidTempAllowlistStateLSP(targetUid, true); mUiHandler.obtainMessage(PUSH_TEMP_ALLOWLIST_UI_MSG).sendToTarget(); - if (type == TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED) { + if (type == TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED) { mFgsStartTempAllowList.add(targetUid, duration, new FgsTempAllowListItem(duration, reasonCode, reason, callingUid)); } @@ -15285,7 +15291,7 @@ public class ActivityManagerService extends IActivityManager.Stub synchronized (mProcLock) { mDeviceIdleTempAllowlist = appids; if (adding) { - if (type == TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED) { + if (type == TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED) { mFgsStartTempAllowList.add(changingUid, durationMs, new FgsTempAllowListItem(durationMs, reasonCode, reason, callingUid)); @@ -16152,6 +16158,13 @@ public class ActivityManagerService extends IActivityManager.Stub return mServices.canAllowWhileInUsePermissionInFgsLocked(pid, uid, packageName); } } + + @Override + public @TempAllowListType int getPushMessagingOverQuotaBehavior() { + synchronized (ActivityManagerService.this) { + return mConstants.mPushMessagingOverQuotaBehavior; + } + } } long inputDispatchingTimedOut(int pid, final boolean aboveSystem, String reason) { diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 5a9c4dea4703..2a39326d6b92 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -792,7 +792,7 @@ public final class ProcessList { mAppDataIsolationEnabled = SystemProperties.getBoolean(ANDROID_APP_DATA_ISOLATION_ENABLED_PROPERTY, true); mVoldAppDataIsolationEnabled = SystemProperties.getBoolean( - ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, false); + ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, true); mAppDataIsolationAllowlistedApps = new ArrayList<>( SystemConfig.getInstance().getAppDataIsolationWhitelistedApps()); diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java index 52388ff2877c..9396241c3f98 100644 --- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java +++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java @@ -393,6 +393,7 @@ public final class AppHibernationService extends SystemService { Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "unhibernatePackage"); pkgState.hibernated = false; pkgState.lastUnhibernatedMs = System.currentTimeMillis(); + final long caller = Binder.clearCallingIdentity(); // Deliver LOCKED_BOOT_COMPLETE AND BOOT_COMPLETE broadcast so app can re-register // their alarms/jobs/etc. try { @@ -435,8 +436,10 @@ public final class AppHibernationService extends SystemService { userId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); + } finally { + Binder.restoreCallingIdentity(caller); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } - Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } /** diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java index d98298cbef5a..fa1820456fb9 100644 --- a/services/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java @@ -94,7 +94,7 @@ public class SystemBackupAgent extends BackupAgentHelper { mUserId = user.getIdentifier(); addHelper(SYNC_SETTINGS_HELPER, new AccountSyncSettingsBackupHelper(this, mUserId)); - addHelper(PREFERRED_HELPER, new PreferredActivityBackupHelper()); + addHelper(PREFERRED_HELPER, new PreferredActivityBackupHelper(mUserId)); addHelper(NOTIFICATION_HELPER, new NotificationBackupHelper(mUserId)); addHelper(PERMISSION_HELPER, new PermissionBackupHelper(mUserId)); addHelper(USAGE_STATS_HELPER, new UsageStatsBackupHelper(this)); diff --git a/services/core/java/com/android/server/biometrics/sensors/RevokeChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/RevokeChallengeClient.java index 90fa1b4ad1d6..7f86c628028a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/RevokeChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/RevokeChallengeClient.java @@ -42,7 +42,6 @@ public abstract class RevokeChallengeClient<T> extends HalClientMonitor<T> { super.start(callback); startHalOperation(); - mCallback.onClientFinished(this, true /* success */); } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java index 8cbb896f80a2..5804622a545f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java @@ -45,6 +45,7 @@ public class FaceGenerateChallengeClient extends GenerateChallengeClient<ISessio getFreshDaemon().generateChallenge(); } catch (RemoteException e) { Slog.e(TAG, "Unable to generateChallenge", e); + mCallback.onClientFinished(this, false /* success */); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceRevokeChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceRevokeChallengeClient.java index 229417361ddb..99bf893d5ce8 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceRevokeChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceRevokeChallengeClient.java @@ -48,6 +48,7 @@ public class FaceRevokeChallengeClient extends RevokeChallengeClient<ISession> { getFreshDaemon().revokeChallenge(mChallenge); } catch (RemoteException e) { Slog.e(TAG, "Unable to revokeChallenge", e); + mCallback.onClientFinished(this, false /* success */); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClient.java index 72c5ee5e78c4..24af817a262f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGenerateChallengeClient.java @@ -67,6 +67,7 @@ public class FaceGenerateChallengeClient extends GenerateChallengeClient<IBiomet } } catch (RemoteException e) { Slog.e(TAG, "generateChallenge failed", e); + mCallback.onClientFinished(this, false /* success */); } } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceRevokeChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceRevokeChallengeClient.java index 28580dece284..ff3e770cdcb2 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceRevokeChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceRevokeChallengeClient.java @@ -43,8 +43,10 @@ public class FaceRevokeChallengeClient extends RevokeChallengeClient<IBiometrics protected void startHalOperation() { try { getFreshDaemon().revokeChallenge(); + mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "revokeChallenge failed", e); + mCallback.onClientFinished(this, false /* success */); } } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java index 83c64210d6f6..15a85e6dc309 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java @@ -47,18 +47,17 @@ class FingerprintGenerateChallengeClient extends GenerateChallengeClient<ISessio getFreshDaemon().generateChallenge(); } catch (RemoteException e) { Slog.e(TAG, "Unable to generateChallenge", e); + mCallback.onClientFinished(this, false /* success */); } } void onChallengeGenerated(int sensorId, int userId, long challenge) { try { getListener().onChallengeGenerated(sensorId, challenge); - mCallback.onClientFinished(FingerprintGenerateChallengeClient.this, - true /* success */); + mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "Unable to send challenge", e); - mCallback.onClientFinished(FingerprintGenerateChallengeClient.this, - false /* success */); + mCallback.onClientFinished(this, false /* success */); } } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRevokeChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRevokeChallengeClient.java index d9bf1c3aa303..90c69789b541 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRevokeChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintRevokeChallengeClient.java @@ -48,6 +48,7 @@ class FingerprintRevokeChallengeClient extends RevokeChallengeClient<ISession> { getFreshDaemon().revokeChallenge(mChallenge); } catch (RemoteException e) { Slog.e(TAG, "Unable to revokeChallenge", e); + mCallback.onClientFinished(this, false /* success */); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintGenerateChallengeClient.java index 5169c7de8cdd..302ec2bd4c81 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintGenerateChallengeClient.java @@ -55,6 +55,7 @@ public class FingerprintGenerateChallengeClient } } catch (RemoteException e) { Slog.e(TAG, "preEnroll failed", e); + mCallback.onClientFinished(this, false /* success */); } } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintRevokeChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintRevokeChallengeClient.java index 8f58cae51575..93d8ff3db177 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintRevokeChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintRevokeChallengeClient.java @@ -45,8 +45,10 @@ public class FingerprintRevokeChallengeClient protected void startHalOperation() { try { getFreshDaemon().postEnroll(); + mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "revokeChallenge/postEnroll failed", e); + mCallback.onClientFinished(this, false /* success */); } } } diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java index e2aa07102ba3..ab67b138fdbf 100644 --- a/services/core/java/com/android/server/clipboard/ClipboardService.java +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -606,6 +606,10 @@ public class ClipboardService extends SystemService { description.setTimestamp(System.currentTimeMillis()); } } + sendClipChangedBroadcast(clipboard); + } + + private void sendClipChangedBroadcast(PerUserClipboard clipboard) { final long ident = Binder.clearCallingIdentity(); final int n = clipboard.primaryClipListeners.beginBroadcast(); try { @@ -615,7 +619,7 @@ public class ClipboardService extends SystemService { clipboard.primaryClipListeners.getBroadcastCookie(i); if (clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, li.mPackageName, - li.mUid, UserHandle.getUserId(li.mUid))) { + li.mUid, UserHandle.getUserId(li.mUid))) { clipboard.primaryClipListeners.getBroadcastItem(i) .dispatchPrimaryClipChanged(); } @@ -632,7 +636,8 @@ public class ClipboardService extends SystemService { @GuardedBy("mLock") private void startClassificationLocked(@NonNull ClipData clip, @UserIdInt int userId) { - if (clip.getItemCount() == 0) { + CharSequence text = (clip.getItemCount() == 0) ? null : clip.getItemAt(0).getText(); + if (TextUtils.isEmpty(text) || text.length() > mMaxClassificationLength) { clip.getDescription().setClassificationStatus( ClipDescription.CLASSIFICATION_NOT_PERFORMED); return; @@ -650,20 +655,17 @@ public class ClipboardService extends SystemService { } finally { Binder.restoreCallingIdentity(ident); } - CharSequence text = clip.getItemAt(0).getText(); - if (TextUtils.isEmpty(text) || text.length() > mMaxClassificationLength - || text.length() > classifier.getMaxGenerateLinksTextLength()) { + if (text.length() > classifier.getMaxGenerateLinksTextLength()) { clip.getDescription().setClassificationStatus( ClipDescription.CLASSIFICATION_NOT_PERFORMED); return; } - getClipboardLocked(userId).mTextClassifier = classifier; - mWorkerHandler.post(() -> doClassification(text, clip, classifier)); + mWorkerHandler.post(() -> doClassification(text, clip, classifier, userId)); } @WorkerThread private void doClassification( - CharSequence text, ClipData clip, TextClassifier classifier) { + CharSequence text, ClipData clip, TextClassifier classifier, @UserIdInt int userId) { TextLinks.Request request = new TextLinks.Request.Builder(text).build(); TextLinks links = classifier.generateLinks(request); @@ -680,13 +682,53 @@ public class ClipboardService extends SystemService { } synchronized (mLock) { - clip.getDescription().setConfidenceScores(confidences); - if (!links.getLinks().isEmpty()) { - clip.getItemAt(0).setTextLinks(links); + PerUserClipboard clipboard = getClipboardLocked(userId); + if (clipboard.primaryClip == clip) { + applyClassificationAndSendBroadcastLocked( + clipboard, confidences, links, classifier); + + // Also apply to related profiles if needed + List<UserInfo> related = getRelatedProfiles(userId); + if (related != null) { + int size = related.size(); + for (int i = 0; i < size; i++) { + int id = related.get(i).id; + if (id != userId) { + final boolean canCopyIntoProfile = !hasRestriction( + UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, id); + if (canCopyIntoProfile) { + PerUserClipboard relatedClipboard = getClipboardLocked(id); + if (hasTextLocked(relatedClipboard, text)) { + applyClassificationAndSendBroadcastLocked( + relatedClipboard, confidences, links, classifier); + } + } + } + } + } } } } + @GuardedBy("mLock") + private void applyClassificationAndSendBroadcastLocked( + PerUserClipboard clipboard, ArrayMap<String, Float> confidences, TextLinks links, + TextClassifier classifier) { + clipboard.mTextClassifier = classifier; + clipboard.primaryClip.getDescription().setConfidenceScores(confidences); + if (!links.getLinks().isEmpty()) { + clipboard.primaryClip.getItemAt(0).setTextLinks(links); + } + sendClipChangedBroadcast(clipboard); + } + + @GuardedBy("mLock") + private boolean hasTextLocked(PerUserClipboard clipboard, @NonNull CharSequence text) { + return clipboard.primaryClip != null + && clipboard.primaryClip.getItemCount() > 0 + && text.equals(clipboard.primaryClip.getItemAt(0).getText()); + } + private boolean isDeviceLocked(@UserIdInt int userId) { final long token = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 91b96dc17473..1a07cb854cae 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -738,13 +738,19 @@ class AutomaticBrightnessController { float value = mBrightnessMapper.getBrightness(mAmbientLux, mForegroundAppPackageName, mForegroundAppCategory); float newScreenAutoBrightness = clampScreenBrightness(value); + + // The min/max range can change for brightness due to HBM. See if the current brightness + // value still falls within the current range (which could have changed). + final boolean currentBrightnessWithinAllowedRange = BrightnessSynchronizer.floatEquals( + mScreenAutoBrightness, clampScreenBrightness(mScreenAutoBrightness)); // If screenAutoBrightness is set, we should have screen{Brightening,Darkening}Threshold, // in which case we ignore the new screen brightness if it doesn't differ enough from the // previous one. if (!Float.isNaN(mScreenAutoBrightness) && !isManuallySet && newScreenAutoBrightness > mScreenDarkeningThreshold - && newScreenAutoBrightness < mScreenBrighteningThreshold) { + && newScreenAutoBrightness < mScreenBrighteningThreshold + && currentBrightnessWithinAllowedRange) { if (mLoggingEnabled) { Slog.d(TAG, "ignoring newScreenAutoBrightness: " + mScreenDarkeningThreshold + " < " + newScreenAutoBrightness diff --git a/services/core/java/com/android/server/display/DisplayBlanker.java b/services/core/java/com/android/server/display/DisplayBlanker.java index e2129ba13626..8de49af3de22 100644 --- a/services/core/java/com/android/server/display/DisplayBlanker.java +++ b/services/core/java/com/android/server/display/DisplayBlanker.java @@ -20,5 +20,8 @@ package com.android.server.display; * Interface used to update the actual display state. */ public interface DisplayBlanker { - void requestDisplayState(int displayId, int state, float brightness); + /** + * Requests the specified display state and brightness levels for the specified displayId. + */ + void requestDisplayState(int displayId, int state, float brightness, float sdrBrightness); } diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java index b3070b7cf1ba..35f29579b417 100644 --- a/services/core/java/com/android/server/display/DisplayDevice.java +++ b/services/core/java/com/android/server/display/DisplayDevice.java @@ -156,10 +156,12 @@ abstract class DisplayDevice { * * @param state The new display state. * @param brightnessState The new display brightnessState. + * @param sdrBrightnessState The new display brightnessState for SDR layers. * @return A runnable containing work to be deferred until after we have * exited the critical section, or null if none. */ - public Runnable requestDisplayStateLocked(int state, float brightnessState) { + public Runnable requestDisplayStateLocked(int state, float brightnessState, + float sdrBrightnessState) { return null; } diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 2d7145fef69c..c46cfe3d1d2f 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -104,6 +104,7 @@ public class DisplayDeviceConfig { private float mBrightnessRampSlowIncrease = Float.NaN; private Spline mBrightnessToBacklightSpline; private Spline mBacklightToBrightnessSpline; + private Spline mBacklightToNitsSpline; private List<String> mQuirks; private boolean mIsHighBrightnessModeEnabled = false; private HighBrightnessModeData mHbmData; @@ -219,6 +220,20 @@ public class DisplayDeviceConfig { } /** + * Calculates the nits value for the specified backlight value if a mapping exists. + * + * @return The mapped nits or 0 if no mapping exits. + */ + public float getNitsFromBacklight(float backlight) { + if (mBacklightToNitsSpline == null) { + Slog.wtf(TAG, "requesting nits when no mapping exists."); + return -1; + } + backlight = Math.max(backlight, mBacklightMinimum); + return mBacklightToNitsSpline.interpolate(backlight); + } + + /** * Return an array of equal length to backlight and nits, that covers the entire system * brightness range of 0.0-1.0. * @@ -258,6 +273,13 @@ public class DisplayDeviceConfig { } /** + * @return true if a nits to backlight mapping is defined in this config, false otherwise. + */ + public boolean hasNitsMapping() { + return mBacklightToNitsSpline != null; + } + + /** * @param quirkValue The quirk to test. * @return {@code true} if the specified quirk is present in this configuration, * {@code false} otherwise. @@ -584,6 +606,7 @@ public class DisplayDeviceConfig { } mBrightnessToBacklightSpline = Spline.createSpline(mBrightness, mBacklight); mBacklightToBrightnessSpline = Spline.createSpline(mBacklight, mBrightness); + mBacklightToNitsSpline = Spline.createSpline(mBacklight, mNits); } private void loadQuirks(DisplayConfiguration config) { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 789f08fb3187..0a4b137fa6cd 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -257,7 +257,8 @@ public final class DisplayManagerService extends SystemService { private final DisplayBlanker mDisplayBlanker = new DisplayBlanker() { // Synchronized to avoid race conditions when updating multiple display states. @Override - public synchronized void requestDisplayState(int displayId, int state, float brightness) { + public synchronized void requestDisplayState(int displayId, int state, float brightness, + float sdrBrightness) { boolean allInactive = true; boolean allOff = true; final boolean stateChanged; @@ -288,7 +289,7 @@ public final class DisplayManagerService extends SystemService { // The order of operations is important for legacy reasons. if (state == Display.STATE_OFF) { - requestDisplayStateInternal(displayId, state, brightness); + requestDisplayStateInternal(displayId, state, brightness, sdrBrightness); } if (stateChanged) { @@ -296,7 +297,7 @@ public final class DisplayManagerService extends SystemService { } if (state != Display.STATE_OFF) { - requestDisplayStateInternal(displayId, state, brightness); + requestDisplayStateInternal(displayId, state, brightness, sdrBrightness); } } }; @@ -316,7 +317,7 @@ public final class DisplayManagerService extends SystemService { // A map from LogicalDisplay ID to display brightness. @GuardedBy("mSyncRoot") - private final SparseArray<Float> mDisplayBrightnesses = new SparseArray<>(); + private final SparseArray<BrightnessPair> mDisplayBrightnesses = new SparseArray<>(); // Set to true when there are pending display changes that have yet to be applied // to the surface flinger state. @@ -667,11 +668,8 @@ public final class DisplayManagerService extends SystemService { } } - private void requestDisplayStateInternal(int displayId, int state, float brightnessState) { - if (state == Display.STATE_UNKNOWN) { - state = Display.STATE_ON; - } - if (state == Display.STATE_OFF) { + private float clampBrightness(int displayState, float brightnessState) { + if (displayState == Display.STATE_OFF) { brightnessState = PowerManager.BRIGHTNESS_OFF_FLOAT; } else if (brightnessState != PowerManager.BRIGHTNESS_OFF_FLOAT && brightnessState < PowerManager.BRIGHTNESS_MIN) { @@ -679,6 +677,17 @@ public final class DisplayManagerService extends SystemService { } else if (brightnessState > PowerManager.BRIGHTNESS_MAX) { brightnessState = PowerManager.BRIGHTNESS_MAX; } + return brightnessState; + } + + private void requestDisplayStateInternal(int displayId, int state, float brightnessState, + float sdrBrightnessState) { + if (state == Display.STATE_UNKNOWN) { + state = Display.STATE_ON; + } + + brightnessState = clampBrightness(state, brightnessState); + sdrBrightnessState = clampBrightness(state, sdrBrightnessState); // Update the display state within the lock. // Note that we do not need to schedule traversals here although it @@ -688,20 +697,26 @@ public final class DisplayManagerService extends SystemService { synchronized (mSyncRoot) { final int index = mDisplayStates.indexOfKey(displayId); + final BrightnessPair brightnessPair = + index < 0 ? null : mDisplayBrightnesses.valueAt(index); if (index < 0 || (mDisplayStates.valueAt(index) == state - && BrightnessSynchronizer.floatEquals(mDisplayBrightnesses.valueAt(index), - brightnessState))) { + && BrightnessSynchronizer.floatEquals( + brightnessPair.brightness, brightnessState) + && BrightnessSynchronizer.floatEquals( + brightnessPair.sdrBrightness, sdrBrightnessState))) { return; // Display no longer exists or no change. } traceMessage = "requestDisplayStateInternal(" + displayId + ", " + Display.stateToString(state) - + ", brightness=" + brightnessState + ")"; + + ", brightness=" + brightnessState + + ", sdrBrightness=" + sdrBrightnessState + ")"; Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, traceMessage, displayId); mDisplayStates.setValueAt(index, state); - mDisplayBrightnesses.setValueAt(index, brightnessState); + brightnessPair.brightness = brightnessState; + brightnessPair.sdrBrightness = sdrBrightnessState; runnable = updateDisplayStateLocked(mLogicalDisplayMapper.getDisplayLocked(displayId) .getPrimaryDisplayDeviceLocked()); } @@ -1235,7 +1250,9 @@ public final class DisplayManagerService extends SystemService { addDisplayPowerControllerLocked(display); mDisplayStates.append(displayId, Display.STATE_UNKNOWN); - mDisplayBrightnesses.append(displayId, display.getDisplayInfoLocked().brightnessDefault); + final float brightnessDefault = display.getDisplayInfoLocked().brightnessDefault; + mDisplayBrightnesses.append(displayId, + new BrightnessPair(brightnessDefault, brightnessDefault)); DisplayManagerGlobal.invalidateLocalDisplayInfoCaches(); @@ -1265,11 +1282,6 @@ public final class DisplayManagerService extends SystemService { // this point. sendDisplayEventLocked(displayId, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); scheduleTraversalLocked(false); - - DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); - if (dpc != null) { - dpc.onDisplayChanged(); - } } private void handleLogicalDisplayFrameRateOverridesChangedLocked( @@ -1301,6 +1313,11 @@ public final class DisplayManagerService extends SystemService { if (work != null) { mHandler.post(work); } + final int displayId = display.getDisplayIdLocked(); + DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); + if (dpc != null) { + dpc.onDisplayChanged(); + } handleLogicalDisplayChangedLocked(display); } @@ -1326,8 +1343,9 @@ public final class DisplayManagerService extends SystemService { // Only send a request for display state if display state has already been initialized. if (state != Display.STATE_UNKNOWN) { - final float brightness = mDisplayBrightnesses.get(displayId); - return device.requestDisplayStateLocked(state, brightness); + final BrightnessPair brightnessPair = mDisplayBrightnesses.get(displayId); + return device.requestDisplayStateLocked(state, brightnessPair.brightness, + brightnessPair.sdrBrightness); } } return null; @@ -1935,10 +1953,11 @@ public final class DisplayManagerService extends SystemService { for (int i = 0; i < displayStateCount; i++) { final int displayId = mDisplayStates.keyAt(i); final int displayState = mDisplayStates.valueAt(i); - final float brightness = mDisplayBrightnesses.valueAt(i); + final BrightnessPair brightnessPair = mDisplayBrightnesses.valueAt(i); pw.println(" Display Id=" + displayId); pw.println(" Display State=" + Display.stateToString(displayState)); - pw.println(" Display Brightness=" + brightness); + pw.println(" Display Brightness=" + brightnessPair.brightness); + pw.println(" Display SdrBrightness=" + brightnessPair.sdrBrightness); } IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); @@ -3277,6 +3296,16 @@ public final class DisplayManagerService extends SystemService { } }; + private class BrightnessPair { + public float brightness; + public float sdrBrightness; + + BrightnessPair(float brightness, float sdrBrightness) { + this.brightness = brightness; + this.sdrBrightness = sdrBrightness; + } + } + /** * Functional interface for providing time. * TODO(b/184781936): merge with PowerManagerService.Clock @@ -3288,5 +3317,4 @@ public final class DisplayManagerService extends SystemService { */ long uptimeMillis(); } - } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 3340e3c73fa1..7a50a34ae4ad 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -38,6 +38,7 @@ import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest; import android.metrics.LogMaker; import android.net.Uri; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.PowerManager; @@ -61,6 +62,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.server.LocalServices; import com.android.server.am.BatteryStatsService; +import com.android.server.display.RampAnimator.DualRampAnimator; import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal; import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener; import com.android.server.display.whitebalance.DisplayWhiteBalanceController; @@ -223,6 +225,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call @GuardedBy("mCachedBrightnessInfo") private final CachedBrightnessInfo mCachedBrightnessInfo = new CachedBrightnessInfo(); + private DisplayDevice mDisplayDevice; + // True if we should fade the screen while turning it off, false if we should play // a stylish color fade animation instead. private boolean mColorFadeFadesConfig; @@ -424,7 +428,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // Animators. private ObjectAnimator mColorFadeOnAnimator; private ObjectAnimator mColorFadeOffAnimator; - private RampAnimator<DisplayPowerState> mScreenBrightnessRampAnimator; + private DualRampAnimator<DisplayPowerState> mScreenBrightnessRampAnimator; private BrightnessSetting.BrightnessSettingListener mBrightnessSettingListener; // True if this DisplayPowerController has been stopped and should no longer be running. @@ -442,6 +446,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call Runnable onBrightnessChangeRunnable) { mLogicalDisplay = logicalDisplay; mDisplayId = mLogicalDisplay.getDisplayIdLocked(); + mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); mHandler = new DisplayControllerHandler(handler.getLooper()); if (mDisplayId == Display.DEFAULT_DISPLAY) { @@ -780,12 +785,29 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call * when displays get swapped on foldable devices. For example, different brightness properties * of each display need to be properly reflected in AutomaticBrightnessController. */ + @GuardedBy("DisplayManagerService.mSyncRoot") public void onDisplayChanged() { - // TODO: b/175821789 - Support high brightness on multiple (folding) displays - mUniqueDisplayId = mLogicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(); - mDisplayDeviceConfig = mLogicalDisplay.getPrimaryDisplayDeviceLocked() - .getDisplayDeviceConfig(); - loadAmbientLightSensor(); + final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); + if (device == null) { + Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: " + + mLogicalDisplay.getDisplayIdLocked()); + return; + } + + final String uniqueId = device.getUniqueId(); + final DisplayDeviceConfig config = device.getDisplayDeviceConfig(); + final IBinder token = device.getDisplayTokenLocked(); + mHandler.post(() -> { + if (mDisplayDevice == device) { + return; + } + mDisplayDevice = device; + mUniqueDisplayId = uniqueId; + mDisplayDeviceConfig = config; + + loadAmbientLightSensor(); + mHbmController.resetHbmData(token, config.getHighBrightnessModeData()); + }); } /** @@ -855,8 +877,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mColorFadeOffAnimator.addListener(mAnimatorListener); } - mScreenBrightnessRampAnimator = new RampAnimator<>( - mPowerState, DisplayPowerState.SCREEN_BRIGHTNESS_FLOAT); + mScreenBrightnessRampAnimator = new DualRampAnimator<>(mPowerState, + DisplayPowerState.SCREEN_BRIGHTNESS_FLOAT, + DisplayPowerState.SCREEN_SDR_BRIGHTNESS_FLOAT); mScreenBrightnessRampAnimator.setListener(mRampAnimatorListener); noteScreenState(mPowerState.getScreenState()); @@ -902,6 +925,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call /** Clean up all resources that are accessed via the {@link #mHandler} thread. */ private void cleanupHandlerThreadAfterStop() { setProximitySensorEnabled(false); + mHbmController.stop(); mHandler.removeCallbacksAndMessages(null); if (mUnfinishedBusiness) { mCallbacks.releaseSuspendBlocker(); @@ -1205,9 +1229,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // timeout is about to expire. if (mPowerRequest.policy == DisplayPowerRequest.POLICY_DIM) { if (brightnessState > PowerManager.BRIGHTNESS_MIN) { - brightnessState = Math.max(Math.min(brightnessState - - SCREEN_DIM_MINIMUM_REDUCTION_FLOAT, - mScreenBrightnessDimConfig), PowerManager.BRIGHTNESS_MIN); + brightnessState = Math.max( + Math.min(brightnessState - SCREEN_DIM_MINIMUM_REDUCTION_FLOAT, + mScreenBrightnessDimConfig), + PowerManager.BRIGHTNESS_MIN); mBrightnessReasonTemp.addModifier(BrightnessReason.MODIFIER_DIMMED); } if (!mAppliedDimming) { @@ -1282,12 +1307,27 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // transformations to the brightness have pushed it outside of the currently // allowed range. float animateValue = clampScreenBrightness(brightnessState); + + // If there are any HDR layers on the screen, we have a special brightness value that we + // use instead. We still preserve the calculated brightness for Standard Dynamic Range + // (SDR) layers, but the main brightness value will be the one for HDR. + float sdrAnimateValue = animateValue; + if (mHbmController.getHighBrightnessMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR + && ((mBrightnessReason.modifier & BrightnessReason.MODIFIER_DIMMED) == 0 + || (mBrightnessReason.modifier & BrightnessReason.MODIFIER_LOW_POWER) == 0)) { + animateValue = mHbmController.getHdrBrightnessValue(); + } + final float currentBrightness = mPowerState.getScreenBrightness(); + final float currentSdrBrightness = mPowerState.getSdrScreenBrightness(); if (isValidBrightnessValue(animateValue) - && !BrightnessSynchronizer.floatEquals(animateValue, currentBrightness)) { + && (!BrightnessSynchronizer.floatEquals(animateValue, currentBrightness) + || !BrightnessSynchronizer.floatEquals( + sdrAnimateValue, currentSdrBrightness))) { if (initialRampSkip || hasBrightnessBuckets || wasOrWillBeInVr || !isDisplayContentVisible || brightnessIsTemporary) { - animateScreenBrightness(animateValue, SCREEN_ANIMATION_RATE_MINIMUM); + animateScreenBrightness(animateValue, sdrAnimateValue, + SCREEN_ANIMATION_RATE_MINIMUM); } else { boolean isIncreasing = animateValue > currentBrightness; final float rampSpeed; @@ -1300,7 +1340,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } else { rampSpeed = mBrightnessRampRateFastDecrease; } - animateScreenBrightness(animateValue, rampSpeed); + animateScreenBrightness(animateValue, sdrAnimateValue, rampSpeed); } } @@ -1446,9 +1486,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private HighBrightnessModeController createHbmController() { final DisplayDeviceConfig ddConfig = mLogicalDisplay.getPrimaryDisplayDeviceLocked().getDisplayDeviceConfig(); + final IBinder displayToken = + mLogicalDisplay.getPrimaryDisplayDeviceLocked().getDisplayTokenLocked(); final DisplayDeviceConfig.HighBrightnessModeData hbmData = ddConfig != null ? ddConfig.getHighBrightnessModeData() : null; - return new HighBrightnessModeController(mHandler, PowerManager.BRIGHTNESS_MIN, + return new HighBrightnessModeController(mHandler, displayToken, PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, hbmData, () -> { sendUpdatePowerStateLocked(); @@ -1596,11 +1638,12 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call && brightnessState <= PowerManager.BRIGHTNESS_MAX; } - private void animateScreenBrightness(float target, float rate) { + private void animateScreenBrightness(float target, float sdrTarget, float rate) { if (DEBUG) { - Slog.d(TAG, "Animating brightness: target=" + target +", rate=" + rate); + Slog.d(TAG, "Animating brightness: target=" + target + ", sdrTarget=" + sdrTarget + + ", rate=" + rate); } - if (mScreenBrightnessRampAnimator.animateTo(target, rate)) { + if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate)) { Trace.traceCounter(Trace.TRACE_TAG_POWER, "TargetScreenBrightness", (int) target); // TODO(b/153319140) remove when we can get this from the above trace invocation SystemProperties.set("debug.tracing.screen_brightness", String.valueOf(target)); @@ -2295,6 +2338,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return; } handleSettingsChange(false /*userSwitch*/); + break; } } } @@ -2392,7 +2436,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call static final int MODIFIER_DIMMED = 0x1; static final int MODIFIER_LOW_POWER = 0x2; - static final int MODIFIER_MASK = 0x3; + static final int MODIFIER_HDR = 0x4; + static final int MODIFIER_MASK = MODIFIER_DIMMED | MODIFIER_LOW_POWER | MODIFIER_HDR; // ADJUSTMENT_* // These things can happen at any point, even if the main brightness reason doesn't @@ -2464,6 +2509,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if ((modifier & MODIFIER_DIMMED) != 0) { sb.append(" dim"); } + if ((modifier & MODIFIER_HDR) != 0) { + sb.append(" hdr"); + } int strlen = sb.length(); if (sb.charAt(strlen - 1) == '[') { sb.setLength(strlen - 2); diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java index 77aff5b03dd2..b58dd38348aa 100644 --- a/services/core/java/com/android/server/display/DisplayPowerState.java +++ b/services/core/java/com/android/server/display/DisplayPowerState.java @@ -62,6 +62,7 @@ final class DisplayPowerState { private int mScreenState; private float mScreenBrightness; + private float mSdrScreenBrightness; private boolean mScreenReady; private boolean mScreenUpdatePending; @@ -92,6 +93,7 @@ final class DisplayPowerState { mScreenState = displayState; mScreenBrightness = (displayState != Display.STATE_OFF) ? PowerManager.BRIGHTNESS_MAX : PowerManager.BRIGHTNESS_OFF_FLOAT; + mSdrScreenBrightness = mScreenBrightness; scheduleScreenUpdate(); mColorFadePrepared = false; @@ -126,6 +128,19 @@ final class DisplayPowerState { } }; + public static final FloatProperty<DisplayPowerState> SCREEN_SDR_BRIGHTNESS_FLOAT = + new FloatProperty<DisplayPowerState>("sdrScreenBrightnessFloat") { + @Override + public void setValue(DisplayPowerState object, float value) { + object.setSdrScreenBrightness(value); + } + + @Override + public Float get(DisplayPowerState object) { + return object.getSdrScreenBrightness(); + } + }; + /** * Sets whether the screen is on, off, or dozing. */ @@ -149,12 +164,38 @@ final class DisplayPowerState { } /** + * Sets the display's SDR brightness. + * + * @param brightness The brightness, ranges from 0.0f (minimum / off) to 1.0f (brightest). + */ + public void setSdrScreenBrightness(float brightness) { + if (!BrightnessSynchronizer.floatEquals(mSdrScreenBrightness, brightness)) { + if (DEBUG) { + Slog.d(TAG, "setSdrScreenBrightness: brightness=" + brightness); + } + + mSdrScreenBrightness = brightness; + if (mScreenState != Display.STATE_OFF) { + mScreenReady = false; + scheduleScreenUpdate(); + } + } + } + + /** + * Gets the screen SDR brightness. + */ + public float getSdrScreenBrightness() { + return mSdrScreenBrightness; + } + + /** * Sets the display brightness. * * @param brightness The brightness, ranges from 0.0f (minimum / off) to 1.0f (brightest). */ public void setScreenBrightness(float brightness) { - if (mScreenBrightness != brightness) { + if (!BrightnessSynchronizer.floatEquals(mScreenBrightness, brightness)) { if (DEBUG) { Slog.d(TAG, "setScreenBrightness: brightness=" + brightness); } @@ -286,6 +327,7 @@ final class DisplayPowerState { pw.println(" mStopped=" + mStopped); pw.println(" mScreenState=" + Display.stateToString(mScreenState)); pw.println(" mScreenBrightness=" + mScreenBrightness); + pw.println(" mSdrScreenBrightness=" + mSdrScreenBrightness); pw.println(" mScreenReady=" + mScreenReady); pw.println(" mScreenUpdatePending=" + mScreenUpdatePending); pw.println(" mColorFadePrepared=" + mColorFadePrepared); @@ -332,7 +374,10 @@ final class DisplayPowerState { float brightnessState = mScreenState != Display.STATE_OFF && mColorFadeLevel > 0f ? mScreenBrightness : PowerManager.BRIGHTNESS_OFF_FLOAT; - if (mPhotonicModulator.setState(mScreenState, brightnessState)) { + float sdrBrightnessState = mScreenState != Display.STATE_OFF + && mColorFadeLevel > 0f + ? mSdrScreenBrightness : PowerManager.BRIGHTNESS_OFF_FLOAT; + if (mPhotonicModulator.setState(mScreenState, brightnessState, sdrBrightnessState)) { if (DEBUG) { Slog.d(TAG, "Screen ready"); } @@ -373,8 +418,10 @@ final class DisplayPowerState { private int mPendingState = INITIAL_SCREEN_STATE; private float mPendingBacklight = INITIAL_BACKLIGHT_FLOAT; + private float mPendingSdrBacklight = INITIAL_BACKLIGHT_FLOAT; private int mActualState = INITIAL_SCREEN_STATE; private float mActualBacklight = INITIAL_BACKLIGHT_FLOAT; + private float mActualSdrBacklight = INITIAL_BACKLIGHT_FLOAT; private boolean mStateChangeInProgress; private boolean mBacklightChangeInProgress; @@ -382,11 +429,13 @@ final class DisplayPowerState { super("PhotonicModulator"); } - public boolean setState(int state, float brightnessState) { + public boolean setState(int state, float brightnessState, float sdrBrightnessState) { synchronized (mLock) { boolean stateChanged = state != mPendingState; - boolean backlightChanged = !BrightnessSynchronizer.floatEquals( - brightnessState, mPendingBacklight); + boolean backlightChanged = + !BrightnessSynchronizer.floatEquals(brightnessState, mPendingBacklight) + || !BrightnessSynchronizer.floatEquals( + sdrBrightnessState, mPendingSdrBacklight); if (stateChanged || backlightChanged) { if (DEBUG) { Slog.d(TAG, "Requesting new screen state: state=" @@ -395,6 +444,7 @@ final class DisplayPowerState { mPendingState = state; mPendingBacklight = brightnessState; + mPendingSdrBacklight = sdrBrightnessState; boolean changeInProgress = mStateChangeInProgress || mBacklightChangeInProgress; mStateChangeInProgress = stateChanged || mStateChangeInProgress; mBacklightChangeInProgress = backlightChanged || mBacklightChangeInProgress; @@ -413,8 +463,10 @@ final class DisplayPowerState { pw.println("Photonic Modulator State:"); pw.println(" mPendingState=" + Display.stateToString(mPendingState)); pw.println(" mPendingBacklight=" + mPendingBacklight); + pw.println(" mPendingSdrBacklight=" + mPendingSdrBacklight); pw.println(" mActualState=" + Display.stateToString(mActualState)); pw.println(" mActualBacklight=" + mActualBacklight); + pw.println(" mActualSdrBacklight=" + mActualSdrBacklight); pw.println(" mStateChangeInProgress=" + mStateChangeInProgress); pw.println(" mBacklightChangeInProgress=" + mBacklightChangeInProgress); } @@ -427,13 +479,17 @@ final class DisplayPowerState { final int state; final boolean stateChanged; final float brightnessState; + final float sdrBrightnessState; final boolean backlightChanged; synchronized (mLock) { state = mPendingState; stateChanged = (state != mActualState); brightnessState = mPendingBacklight; - backlightChanged = !BrightnessSynchronizer.floatEquals( - brightnessState, mActualBacklight); + sdrBrightnessState = mPendingSdrBacklight; + backlightChanged = + !BrightnessSynchronizer.floatEquals(brightnessState, mActualBacklight) + || !BrightnessSynchronizer.floatEquals( + sdrBrightnessState, mActualSdrBacklight); if (!stateChanged) { // State changed applied, notify outer class. postScreenUpdateThreadSafe(); @@ -454,14 +510,17 @@ final class DisplayPowerState { } mActualState = state; mActualBacklight = brightnessState; + mActualSdrBacklight = sdrBrightnessState; } // Apply pending change. if (DEBUG) { Slog.d(TAG, "Updating screen state: id=" + mDisplayId + ", state=" - + Display.stateToString(state) + ", backlight=" + brightnessState); + + Display.stateToString(state) + ", backlight=" + brightnessState + + ", sdrBacklight=" + sdrBrightnessState); } - mBlanker.requestDisplayState(mDisplayId, state, brightnessState); + mBlanker.requestDisplayState(mDisplayId, state, brightnessState, + sdrBrightnessState); } } } diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java index e6486bd2a79a..b9487779dfd3 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeController.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java @@ -18,9 +18,11 @@ package com.android.server.display; import android.hardware.display.BrightnessInfo; import android.os.Handler; +import android.os.IBinder; import android.os.PowerManager; import android.os.SystemClock; import android.util.Slog; +import android.view.SurfaceControlHdrLayerInfoListener; import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData; @@ -45,17 +47,21 @@ class HighBrightnessModeController { private final float mBrightnessMin; private final float mBrightnessMax; - private final HighBrightnessModeData mHbmData; private final Handler mHandler; private final Runnable mHbmChangeCallback; private final Runnable mRecalcRunnable; private final Clock mClock; + private SurfaceControlHdrLayerInfoListener mHdrListener; + private HighBrightnessModeData mHbmData; + private IBinder mRegisteredDisplayToken; + private boolean mIsInAllowedAmbientRange = false; private boolean mIsTimeAvailable = false; private boolean mIsAutoBrightnessEnabled = false; private float mAutoBrightness; private int mHbmMode = BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF; + private boolean mIsHdrLayerPresent = false; /** * If HBM is currently running, this is the start time for the current HBM session. @@ -69,23 +75,26 @@ class HighBrightnessModeController { */ private LinkedList<HbmEvent> mEvents = new LinkedList<>(); - HighBrightnessModeController(Handler handler, float brightnessMin, float brightnessMax, - HighBrightnessModeData hbmData, Runnable hbmChangeCallback) { - this(SystemClock::uptimeMillis, handler, brightnessMin, brightnessMax, hbmData, - hbmChangeCallback); + HighBrightnessModeController(Handler handler, IBinder displayToken, float brightnessMin, + float brightnessMax, HighBrightnessModeData hbmData, Runnable hbmChangeCallback) { + this(SystemClock::uptimeMillis, handler, displayToken, brightnessMin, brightnessMax, + hbmData, hbmChangeCallback); } @VisibleForTesting - HighBrightnessModeController(Clock clock, Handler handler, float brightnessMin, - float brightnessMax, HighBrightnessModeData hbmData, Runnable hbmChangeCallback) { + HighBrightnessModeController(Clock clock, Handler handler, IBinder displayToken, + float brightnessMin, float brightnessMax, HighBrightnessModeData hbmData, + Runnable hbmChangeCallback) { mClock = clock; mHandler = handler; mBrightnessMin = brightnessMin; mBrightnessMax = brightnessMax; - mHbmData = hbmData; mHbmChangeCallback = hbmChangeCallback; mAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; mRecalcRunnable = this::recalculateTimeAllowance; + mHdrListener = new HdrListener(); + + resetHbmData(displayToken, hbmData); } void setAutoBrightnessEnabled(boolean isEnabled) { @@ -117,6 +126,10 @@ class HighBrightnessModeController { } } + float getHdrBrightnessValue() { + return mBrightnessMax; + } + void onAmbientLuxChange(float ambientLux) { if (!deviceSupportsHbm() || !mIsAutoBrightnessEnabled) { return; @@ -138,11 +151,12 @@ class HighBrightnessModeController { // If we are starting or ending a high brightness mode session, store the current // session in mRunningStartTimeMillis, or the old one in mEvents. - final boolean wasOldBrightnessHigh = oldAutoBrightness > mHbmData.transitionPoint; - final boolean isNewBrightnessHigh = mAutoBrightness > mHbmData.transitionPoint; - if (wasOldBrightnessHigh != isNewBrightnessHigh) { + final boolean wasHbmDrainingAvailableTime = mRunningStartTimeMillis != -1; + final boolean shouldHbmDrainAvailableTime = mAutoBrightness > mHbmData.transitionPoint + && !mIsHdrLayerPresent; + if (wasHbmDrainingAvailableTime != shouldHbmDrainAvailableTime) { final long currentTime = mClock.uptimeMillis(); - if (isNewBrightnessHigh) { + if (shouldHbmDrainAvailableTime) { mRunningStartTimeMillis = currentTime; } else { mEvents.addFirst(new HbmEvent(mRunningStartTimeMillis, currentTime)); @@ -161,30 +175,49 @@ class HighBrightnessModeController { return mHbmMode; } + void stop() { + registerHdrListener(null /*displayToken*/); + } + + void resetHbmData(IBinder displayToken, HighBrightnessModeData hbmData) { + mHbmData = hbmData; + unregisterHdrListener(); + if (deviceSupportsHbm()) { + registerHdrListener(displayToken); + recalculateTimeAllowance(); + } + } + void dump(PrintWriter pw) { pw.println("HighBrightnessModeController:"); - pw.println(" mBrightnessMin=" + mBrightnessMin); - pw.println(" mBrightnessMax=" + mBrightnessMax); + pw.println(" mCurrentMin=" + getCurrentBrightnessMin()); + pw.println(" mCurrentMax=" + getCurrentBrightnessMax()); + pw.println(" mHbmMode=" + BrightnessInfo.hbmToString(mHbmMode)); + pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis())); pw.println(" mHbmData=" + mHbmData); pw.println(" mIsInAllowedAmbientRange=" + mIsInAllowedAmbientRange); pw.println(" mIsTimeAvailable= " + mIsTimeAvailable); pw.println(" mIsAutoBrightnessEnabled=" + mIsAutoBrightnessEnabled); pw.println(" mAutoBrightness=" + mAutoBrightness); + pw.println(" mIsHdrLayerPresent=" + mIsHdrLayerPresent); + pw.println(" mBrightnessMin=" + mBrightnessMin); + pw.println(" mBrightnessMax=" + mBrightnessMax); } private boolean isCurrentlyAllowed() { - return mIsAutoBrightnessEnabled && mIsTimeAvailable && mIsInAllowedAmbientRange; + return mIsHdrLayerPresent + || (mIsAutoBrightnessEnabled && mIsTimeAvailable && mIsInAllowedAmbientRange); } private boolean deviceSupportsHbm() { return mHbmData != null; } - /** - * Recalculates the allowable HBM time. - */ - private void recalculateTimeAllowance() { - final long currentTime = mClock.uptimeMillis(); + private long calculateRemainingTime(long currentTime) { + if (!deviceSupportsHbm()) { + return 0; + } + long timeAlreadyUsed = 0; // First, lets see how much time we've taken for any currently running @@ -222,8 +255,15 @@ class HighBrightnessModeController { Slog.d(TAG, "Time already used after all sessions: " + timeAlreadyUsed); } - // See how much allowable time we have left. - final long remainingTime = Math.max(0, mHbmData.timeMaxMillis - timeAlreadyUsed); + return Math.max(0, mHbmData.timeMaxMillis - timeAlreadyUsed); + } + + /** + * Recalculates the allowable HBM time. + */ + private void recalculateTimeAllowance() { + final long currentTime = mClock.uptimeMillis(); + final long remainingTime = calculateRemainingTime(currentTime); // We allow HBM if there is more than the minimum required time available // or if brightness is already in the high range, if there is any time left at all. @@ -242,6 +282,7 @@ class HighBrightnessModeController { // If we are not allowed...timeout when the oldest event moved outside of the timing // window by at least minTime. Basically, we're calculating the soonest time we can // get {@code timeMinMillis} back to us. + final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis; final HbmEvent lastEvent = mEvents.getLast(); final long startTimePlusMinMillis = Math.max(windowstartTimeMillis, lastEvent.startTimeMillis) @@ -278,12 +319,36 @@ class HighBrightnessModeController { } private int calculateHighBrightnessMode() { - if (deviceSupportsHbm() && isCurrentlyAllowed()) { + if (!deviceSupportsHbm()) { + return BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF; + } else if (mIsHdrLayerPresent) { + return BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR; + } else if (isCurrentlyAllowed()) { return BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT; } + return BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF; } + private void registerHdrListener(IBinder displayToken) { + if (mRegisteredDisplayToken == displayToken) { + return; + } + + unregisterHdrListener(); + mRegisteredDisplayToken = displayToken; + if (mRegisteredDisplayToken != null) { + mHdrListener.register(mRegisteredDisplayToken); + } + } + + private void unregisterHdrListener() { + if (mRegisteredDisplayToken != null) { + mHdrListener.unregister(mRegisteredDisplayToken); + mIsHdrLayerPresent = false; + } + } + /** * Represents an event in which High Brightness Mode was enabled. */ @@ -302,4 +367,18 @@ class HighBrightnessModeController { + ((endTimeMillis - startTimeMillis) / 1000) + "]"; } } + + private class HdrListener extends SurfaceControlHdrLayerInfoListener { + @Override + public void onHdrInfoChanged(IBinder displayToken, int numberOfHdrLayers, + int maxW, int maxH, int flags) { + mHandler.post(() -> { + mIsHdrLayerPresent = numberOfHdrLayers > 0; + // Calling the auto-brightness update so that we can recalculate + // auto-brightness with HDR in mind. When HDR layers are present, + // we don't limit auto-brightness' HBM time limits. + onAutoBrightnessChanged(mAutoBrightness); + }); + } + } } diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 2546118f1cc7..754e35eff6c1 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -67,6 +67,8 @@ final class LocalDisplayAdapter extends DisplayAdapter { private static final int NO_DISPLAY_MODE_ID = 0; + private static final float NITS_INVALID = -1; + private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>(); private final Injector mInjector; @@ -190,6 +192,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { private int mState = Display.STATE_UNKNOWN; // This is only set in the runnable returned from requestDisplayStateLocked. private float mBrightnessState = PowerManager.BRIGHTNESS_INVALID_FLOAT; + private float mSdrBrightnessState = PowerManager.BRIGHTNESS_INVALID_FLOAT; private int mDefaultModeId; private int mDefaultModeGroup; private int mActiveModeId; @@ -644,13 +647,15 @@ final class LocalDisplayAdapter extends DisplayAdapter { } @Override - public Runnable requestDisplayStateLocked(final int state, final float brightnessState) { + public Runnable requestDisplayStateLocked(final int state, final float brightnessState, + final float sdrBrightnessState) { // Assume that the brightness is off if the display is being turned off. assert state != Display.STATE_OFF || BrightnessSynchronizer.floatEquals( brightnessState, PowerManager.BRIGHTNESS_OFF_FLOAT); final boolean stateChanged = (mState != state); - final boolean brightnessChanged = (!BrightnessSynchronizer.floatEquals( - mBrightnessState, brightnessState)); + final boolean brightnessChanged = + !(BrightnessSynchronizer.floatEquals(mBrightnessState, brightnessState) + && BrightnessSynchronizer.floatEquals(mSdrBrightnessState, sdrBrightnessState)); if (stateChanged || brightnessChanged) { final long physicalDisplayId = mPhysicalDisplayId; final IBinder token = getDisplayTokenLocked(); @@ -702,8 +707,9 @@ final class LocalDisplayAdapter extends DisplayAdapter { // Apply brightness changes given that we are in a non-suspended state. if (brightnessChanged || vrModeChange) { - setDisplayBrightness(brightnessState); + setDisplayBrightness(brightnessState, sdrBrightnessState); mBrightnessState = brightnessState; + mSdrBrightnessState = sdrBrightnessState; } // Enter the final desired state, possibly suspended. @@ -764,8 +770,8 @@ final class LocalDisplayAdapter extends DisplayAdapter { } } - private void setDisplayBrightness(float brightness) { - // Ensure brightnessState is valid, before processing and sending to + private void setDisplayBrightness(float brightness, float sdrBrightness) { + // Ensure brightnessState is valid before processing and sending to // surface control if (Float.isNaN(brightness)) { return; @@ -774,17 +780,31 @@ final class LocalDisplayAdapter extends DisplayAdapter { if (DEBUG) { Slog.d(TAG, "setDisplayBrightness(" + "id=" + physicalDisplayId - + ", brightness=" + brightness + ")"); + + ", brightness=" + brightness + + ", sdrBrightness=" + sdrBrightness + ")"); } Trace.traceBegin(Trace.TRACE_TAG_POWER, "setDisplayBrightness(" - + "id=" + physicalDisplayId + ", brightness=" + brightness + ")"); + + "id=" + physicalDisplayId + ", brightness=" + brightness + + ", sdrBrightness=" + sdrBrightness + ")"); try { - float backlight = brightnessToBacklight(brightness); - mBacklightAdapter.setBacklight(backlight); + final float backlight = brightnessToBacklight(brightness); + float nits = NITS_INVALID; + float sdrBacklight = PowerManager.BRIGHTNESS_INVALID_FLOAT; + float sdrNits = NITS_INVALID; + if (getDisplayDeviceConfig().hasNitsMapping() + && sdrBrightness != PowerManager.BRIGHTNESS_INVALID_FLOAT) { + nits = backlightToNits(backlight); + sdrBacklight = brightnessToBacklight(sdrBrightness); + sdrNits = backlightToNits(sdrBacklight); + } + mBacklightAdapter.setBacklight(sdrBacklight, sdrNits, backlight, nits); Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenBrightness", BrightnessSynchronizer.brightnessFloatToInt(brightness)); + Trace.traceCounter(Trace.TRACE_TAG_POWER, + "SdrScreenBrightness", + BrightnessSynchronizer.brightnessFloatToInt(sdrBrightness)); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } @@ -793,6 +813,10 @@ final class LocalDisplayAdapter extends DisplayAdapter { private float brightnessToBacklight(float brightness) { return getDisplayDeviceConfig().getBacklightFromBrightness(brightness); } + + private float backlightToNits(float backlight) { + return getDisplayDeviceConfig().getNitsFromBacklight(backlight); + } }; } return null; @@ -1242,6 +1266,13 @@ final class LocalDisplayAdapter extends DisplayAdapter { public boolean setDisplayBrightness(IBinder displayToken, float brightness) { return SurfaceControl.setDisplayBrightness(displayToken, brightness); } + + public boolean setDisplayBrightness(IBinder displayToken, float sdrBacklight, + float sdrNits, float displayBacklight, float displayNits) { + return SurfaceControl.setDisplayBrightness(displayToken, sdrBacklight, sdrNits, + displayBacklight, displayNits); + } + } static class BacklightAdapter { @@ -1273,9 +1304,14 @@ final class LocalDisplayAdapter extends DisplayAdapter { } // Set backlight within min and max backlight values - void setBacklight(float backlight) { + void setBacklight(float sdrBacklight, float sdrNits, float backlight, float nits) { if (mUseSurfaceControlBrightness || mForceSurfaceControl) { - mSurfaceControlProxy.setDisplayBrightness(mDisplayToken, backlight); + if (sdrBacklight == PowerManager.BRIGHTNESS_INVALID_FLOAT) { + mSurfaceControlProxy.setDisplayBrightness(mDisplayToken, backlight); + } else { + mSurfaceControlProxy.setDisplayBrightness(mDisplayToken, sdrBacklight, sdrNits, + backlight, nits); + } } else if (mBacklight != null) { mBacklight.setBrightness(backlight); } diff --git a/services/core/java/com/android/server/display/RampAnimator.java b/services/core/java/com/android/server/display/RampAnimator.java index 26004a8da1a1..20feafa2d19c 100644 --- a/services/core/java/com/android/server/display/RampAnimator.java +++ b/services/core/java/com/android/server/display/RampAnimator.java @@ -26,7 +26,7 @@ import com.android.internal.display.BrightnessSynchronizer; * A custom animator that progressively updates a property value at * a given variable rate until it reaches a particular target value. */ -final class RampAnimator<T> { +class RampAnimator<T> { private final T mObject; private final FloatProperty<T> mProperty; private final Choreographer mChoreographer; @@ -174,4 +174,52 @@ final class RampAnimator<T> { public interface Listener { void onAnimationEnd(); } + + static class DualRampAnimator<T> { + private final RampAnimator<T> mFirst; + private final RampAnimator<T> mSecond; + private final Listener mInternalListener = new Listener() { + @Override + public void onAnimationEnd() { + if (mListener != null && !isAnimating()) { + mListener.onAnimationEnd(); + } + } + }; + + private Listener mListener; + + DualRampAnimator(T object, FloatProperty<T> firstProperty, + FloatProperty<T> secondProperty) { + mFirst = new RampAnimator(object, firstProperty); + mFirst.setListener(mInternalListener); + mSecond = new RampAnimator(object, secondProperty); + mSecond.setListener(mInternalListener); + } + + /** + * Starts animating towards the specified values. + * + * If this is the first time the property is being set or if the rate is 0, + * the value jumps directly to the target. + * + * @param firstTarget The first target value. + * @param secondTarget The second target value. + * @param rate The convergence rate in units per second, or 0 to set the value immediately. + * @return True if either target differs from the previous target. + */ + public boolean animateTo(float firstTarget, float secondTarget, float rate) { + final boolean firstRetval = mFirst.animateTo(firstTarget, rate); + final boolean secondRetval = mSecond.animateTo(secondTarget, rate); + return firstRetval && secondRetval; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public boolean isAnimating() { + return mFirst.isAnimating() && mSecond.isAnimating(); + } + } } diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index 52a810bd8caa..b7931c8a8424 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -293,7 +293,8 @@ public class VirtualDisplayAdapter extends DisplayAdapter { } @Override - public Runnable requestDisplayStateLocked(int state, float brightnessState) { + public Runnable requestDisplayStateLocked(int state, float brightnessState, + float sdrBrightnessState) { if (state != mDisplayState) { mDisplayState = state; if (state == Display.STATE_OFF) { diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java index 38d1aab70550..06a9832f1b54 100644 --- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java +++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java @@ -28,6 +28,7 @@ import android.system.ErrnoException; import android.system.Os; import android.text.FontConfig; import android.util.ArrayMap; +import android.util.AtomicFile; import android.util.Base64; import android.util.Slog; @@ -43,6 +44,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; @@ -120,8 +122,7 @@ final class UpdatableFontDir { private final File mFilesDir; private final FontFileParser mParser; private final FsverityUtil mFsverityUtil; - private final File mConfigFile; - private final File mTmpConfigFile; + private final AtomicFile mConfigFile; private final Supplier<Long> mCurrentTimeSupplier; private final Function<Map<String, File>, FontConfig> mConfigSupplier; @@ -131,13 +132,13 @@ final class UpdatableFontDir { /** * A mutable map containing mapping from font file name (e.g. "NotoColorEmoji.ttf") to {@link * FontFileInfo}. All files in this map are validated, and have higher revision numbers than - * corresponding font files in {@link #mPreinstalledFontDirs}. + * corresponding font files returned by {@link #mConfigSupplier}. */ private final ArrayMap<String, FontFileInfo> mFontFileInfoMap = new ArrayMap<>(); UpdatableFontDir(File filesDir, FontFileParser parser, FsverityUtil fsverityUtil) { this(filesDir, parser, fsverityUtil, new File(CONFIG_XML_FILE), - () -> System.currentTimeMillis(), + System::currentTimeMillis, (map) -> SystemFonts.getSystemFontConfig(map, 0, 0) ); } @@ -149,8 +150,7 @@ final class UpdatableFontDir { mFilesDir = filesDir; mParser = parser; mFsverityUtil = fsverityUtil; - mConfigFile = configFile; - mTmpConfigFile = new File(configFile.getAbsoluteFile() + ".tmp"); + mConfigFile = new AtomicFile(configFile); mCurrentTimeSupplier = currentTimeSupplier; mConfigSupplier = configSupplier; } @@ -166,18 +166,16 @@ final class UpdatableFontDir { mConfigVersion = 1; boolean success = false; try { - PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); - try (FileInputStream fis = new FileInputStream(mConfigFile)) { - PersistentSystemFontConfig.loadFromXml(fis, config); - } catch (IOException | XmlPullParserException e) { - // The font config file is missing on the first boot. Just do nothing. - return; - } + PersistentSystemFontConfig.Config config = readPersistentConfig(); mLastModifiedMillis = config.lastModifiedMillis; File[] dirs = mFilesDir.listFiles(); - if (dirs == null) return; - FontConfig fontConfig = getSystemFontConfig(); + if (dirs == null) { + // mFilesDir should be created by init script. + Slog.e(TAG, "Could not read: " + mFilesDir); + return; + } + FontConfig fontConfig = null; for (File dir : dirs) { if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) { Slog.e(TAG, "Unexpected dir found: " + dir); @@ -194,6 +192,9 @@ final class UpdatableFontDir { return; } FontFileInfo fontFileInfo = validateFontFile(files[0]); + if (fontConfig == null) { + fontConfig = getSystemFontConfig(); + } addFileToMapIfSameOrNewer(fontFileInfo, fontConfig, true /* deleteOldFile */); } success = true; @@ -216,15 +217,9 @@ final class UpdatableFontDir { FileUtils.deleteContents(mFilesDir); mLastModifiedMillis = mCurrentTimeSupplier.get(); - try (FileOutputStream fos = new FileOutputStream(mConfigFile)) { - PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); - config.lastModifiedMillis = mLastModifiedMillis; - PersistentSystemFontConfig.writeToXml(fos, config); - } catch (Exception e) { - throw new SystemFontException( - FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG, - "Failed to write config XML.", e); - } + PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); + config.lastModifiedMillis = mLastModifiedMillis; + writePersistentConfig(config); mConfigVersion++; } @@ -234,6 +229,18 @@ final class UpdatableFontDir { * before this method is called. */ public void update(List<FontUpdateRequest> requests) throws SystemFontException { + for (FontUpdateRequest request : requests) { + switch (request.getType()) { + case FontUpdateRequest.TYPE_UPDATE_FONT_FILE: + Objects.requireNonNull(request.getFd()); + Objects.requireNonNull(request.getSignature()); + break; + case FontUpdateRequest.TYPE_UPDATE_FONT_FAMILY: + Objects.requireNonNull(request.getFontFamily()); + Objects.requireNonNull(request.getFontFamily().getName()); + break; + } + } // Backup the mapping for rollback. ArrayMap<String, FontFileInfo> backupMap = new ArrayMap<>(mFontFileInfoMap); PersistentSystemFontConfig.Config curConfig = readPersistentConfig(); @@ -277,20 +284,7 @@ final class UpdatableFontDir { newConfig.updatedFontDirs.add(info.getRandomizedFontDir().getName()); } newConfig.fontFamilies.addAll(familyMap.values()); - - try (FileOutputStream fos = new FileOutputStream(mTmpConfigFile)) { - PersistentSystemFontConfig.writeToXml(fos, newConfig); - } catch (Exception e) { - throw new SystemFontException( - FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG, - "Failed to write config XML.", e); - } - - if (!mFsverityUtil.rename(mTmpConfigFile, mConfigFile)) { - throw new SystemFontException( - FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG, - "Failed to stage the config file."); - } + writePersistentConfig(newConfig); mConfigVersion++; success = true; } finally { @@ -420,7 +414,7 @@ final class UpdatableFontDir { /** * Add the given {@link FontFileInfo} to {@link #mFontFileInfoMap} if its font revision is * equal to or higher than the revision of currently used font file (either in - * {@link #mFontFileInfoMap} or {@link #mPreinstalledFontDirs}). + * {@link #mFontFileInfoMap} or {@code fontConfig}). */ private boolean addFileToMapIfSameOrNewer(FontFileInfo fontFileInfo, FontConfig fontConfig, boolean deleteOldFile) { @@ -568,7 +562,7 @@ final class UpdatableFontDir { private PersistentSystemFontConfig.Config readPersistentConfig() { PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); - try (FileInputStream fis = new FileInputStream(mConfigFile)) { + try (FileInputStream fis = mConfigFile.openRead()) { PersistentSystemFontConfig.loadFromXml(fis, config); } catch (IOException | XmlPullParserException e) { // The font config file is missing on the first boot. Just do nothing. @@ -576,6 +570,23 @@ final class UpdatableFontDir { return config; } + private void writePersistentConfig(PersistentSystemFontConfig.Config config) + throws SystemFontException { + FileOutputStream fos = null; + try { + fos = mConfigFile.startWrite(); + PersistentSystemFontConfig.writeToXml(fos, config); + mConfigFile.finishWrite(fos); + } catch (IOException e) { + if (fos != null) { + mConfigFile.failWrite(fos); + } + throw new SystemFontException( + FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG, + "Failed to write config XML.", e); + } + } + /* package */ int getConfigVersion() { return mConfigVersion; } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 9a3fe82f2d0f..03153d375d33 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -1782,6 +1782,24 @@ public class PackageManagerService extends IPackageManager.Stub } @Override + public <ExceptionOne extends Exception, ExceptionTwo extends Exception> void + withPackageSettingsThrowing2( + @NonNull Throwing2Consumer<Function<String, PackageSetting>, ExceptionOne, + ExceptionTwo> block) throws ExceptionOne, ExceptionTwo { + final Computer snapshot = snapshotComputer(); + + // This method needs to either lock or not lock consistently throughout the method, + // so if the live computer is returned, force a wrapping sync block. + if (snapshot == mLiveComputer) { + synchronized (mLock) { + block.accept(snapshot::getPackageSetting); + } + } else { + block.accept(snapshot::getPackageSetting); + } + } + + @Override public <Output, ExceptionType extends Exception> Output withPackageSettingsReturningThrowing(@NonNull ThrowingFunction<Function<String, PackageSetting>, Output, ExceptionType> block) throws ExceptionType { @@ -22459,21 +22477,44 @@ public class PackageManagerService extends IPackageManager.Stub } @Override - public byte[] getIntentFilterVerificationBackup(int userId) { + public byte[] getDomainVerificationBackup(int userId) { if (Binder.getCallingUid() != Process.SYSTEM_UID) { - throw new SecurityException("Only the system may call getIntentFilterVerificationBackup()"); + throw new SecurityException("Only the system may call getDomainVerificationBackup()"); } - // TODO(b/170746586) - return null; + try { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + TypedXmlSerializer serializer = Xml.resolveSerializer(output); + mDomainVerificationManager.writeSettings(serializer, true, userId); + return output.toByteArray(); + } + } catch (Exception e) { + if (DEBUG_BACKUP) { + Slog.e(TAG, "Unable to write domain verification for backup", e); + } + return null; + } } @Override - public void restoreIntentFilterVerification(byte[] backup, int userId) { + public void restoreDomainVerification(byte[] backup, int userId) { if (Binder.getCallingUid() != Process.SYSTEM_UID) { throw new SecurityException("Only the system may call restorePreferredActivities()"); } - // TODO(b/170746586) + + try { + ByteArrayInputStream input = new ByteArrayInputStream(backup); + TypedXmlPullParser parser = Xml.resolvePullParser(input); + + // User ID input isn't necessary here as it assumes the user integers match and that + // the only states inside the backup XML are for the target user. + mDomainVerificationManager.restoreSettings(parser); + input.close(); + } catch (Exception e) { + if (DEBUG_BACKUP) { + Slog.e(TAG, "Exception restoring domain verification: " + e.getMessage()); + } + } } @Override diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index b6d4a5b88f8a..f891f0e5441d 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -2339,7 +2339,8 @@ public final class Settings implements Watchable, Snappable { } } - mDomainVerificationManager.writeSettings(serializer); + mDomainVerificationManager.writeSettings(serializer, false /* includeSignatures */, + UserHandle.USER_ALL); mKeySetManagerService.writeKeySetManagerServiceLPr(serializer); @@ -2913,13 +2914,7 @@ public final class Settings implements Watchable, Snappable { } str.close(); - - } catch (XmlPullParserException e) { - mReadMessages.append("Error reading: " + e.toString()); - PackageManagerService.reportSettingsProblem(Log.ERROR, "Error reading settings: " + e); - Slog.wtf(PackageManagerService.TAG, "Error reading package manager settings", e); - - } catch (java.io.IOException e) { + } catch (IOException | XmlPullParserException e) { mReadMessages.append("Error reading: " + e.toString()); PackageManagerService.reportSettingsProblem(Log.ERROR, "Error reading settings: " + e); Slog.wtf(PackageManagerService.TAG, "Error reading package manager settings", e); diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java index 5aed3670e5c7..b54b1698a3f4 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java @@ -234,8 +234,8 @@ public interface DomainVerificationManagerInternal { * This will mutate internal {@link DomainVerificationPkgState} and so will hold the internal * lock. This should never be called from within the domain verification classes themselves. * <p> - * This will NOT call {@link #writeSettings(TypedXmlSerializer)}. That must be handled by the - * caller. + * This will NOT call {@link #writeSettings(TypedXmlSerializer, boolean, int)}. That must be + * handled by the caller. */ void addPackage(@NonNull PackageSetting newPkgSetting); @@ -249,20 +249,27 @@ public interface DomainVerificationManagerInternal { * This will mutate internal {@link DomainVerificationPkgState} and so will hold the internal * lock. This should never be called from within the domain verification classes themselves. * <p> - * This will NOT call {@link #writeSettings(TypedXmlSerializer)}. That must be handled by the - * caller. + * This will NOT call {@link #writeSettings(TypedXmlSerializer, boolean, int)}. That must be + * handled by the caller. */ void migrateState(@NonNull PackageSetting oldPkgSetting, @NonNull PackageSetting newPkgSetting); /** * Serializes the entire internal state. This is equivalent to a full backup of the existing * verification state. This write includes legacy state, as a sibling tag the modern state. + * + * @param includeSignatures Whether to include the package signatures in the output, mainly + * used for backing up the user settings and ensuring they're + * re-attached to the same package. + * @param userId The user to write out. Supports {@link UserHandle#USER_ALL} if all users + * should be written. */ - void writeSettings(@NonNull TypedXmlSerializer serializer) throws IOException; + void writeSettings(@NonNull TypedXmlSerializer serializer, boolean includeSignatures, + @UserIdInt int userId) throws IOException; /** * Read back a list of {@link DomainVerificationPkgState}s previously written by {@link - * #writeSettings(TypedXmlSerializer)}. Assumes that the + * #writeSettings(TypedXmlSerializer, boolean, int)}. Assumes that the * {@link DomainVerificationPersistence#TAG_DOMAIN_VERIFICATIONS} tag has already been entered. * <p> * This is expected to only be used to re-attach states for packages already known to be on the @@ -298,7 +305,7 @@ public interface DomainVerificationManagerInternal { /** * Restore a list of {@link DomainVerificationPkgState}s previously written by {@link - * #writeSettings(TypedXmlSerializer)}. Assumes that the + * #writeSettings(TypedXmlSerializer, boolean, int)}. Assumes that the * {@link DomainVerificationPersistence#TAG_DOMAIN_VERIFICATIONS} * tag has already been entered. * <p> @@ -403,7 +410,7 @@ public interface DomainVerificationManagerInternal { /** * Notify that a settings change has been made and that eventually - * {@link #writeSettings(TypedXmlSerializer)} should be invoked by the parent. + * {@link #writeSettings(TypedXmlSerializer, boolean, int)} should be invoked by the parent. */ void scheduleWriteSettings(); @@ -447,6 +454,15 @@ public interface DomainVerificationManagerInternal { throws ExceptionType; /** + * Variant which throws 2 exceptions. + * @see #withPackageSettings(Consumer) + */ + <ExceptionOne extends Exception, ExceptionTwo extends Exception> void + withPackageSettingsThrowing2( + @NonNull Throwing2Consumer<Function<String, PackageSetting>, ExceptionOne, + ExceptionTwo> block) throws ExceptionOne, ExceptionTwo; + + /** * Variant which returns a value to the caller and throws. * @see #withPackageSettings(Consumer) */ @@ -461,6 +477,11 @@ public interface DomainVerificationManagerInternal { void accept(Input input) throws ExceptionType; } + interface Throwing2Consumer<Input, ExceptionOne extends Exception, + ExceptionTwo extends Exception> { + void accept(Input input) throws ExceptionOne, ExceptionTwo; + } + interface ThrowingFunction<Input, Output, ExceptionType extends Exception> { Output apply(Input input) throws ExceptionType; } diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java index f0ad98c26ede..e803457fcb97 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java @@ -18,8 +18,10 @@ package com.android.server.pm.verify.domain; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.content.pm.Signature; import android.content.pm.verify.domain.DomainVerificationState; +import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -77,7 +79,8 @@ public class DomainVerificationPersistence { @NonNull DomainVerificationStateMap<DomainVerificationPkgState> attached, @NonNull ArrayMap<String, DomainVerificationPkgState> pending, @NonNull ArrayMap<String, DomainVerificationPkgState> restored, - @Nullable Function<String, String> pkgNameToSignature) throws IOException { + @UserIdInt int userId, @Nullable Function<String, String> pkgNameToSignature) + throws IOException { try (SettingsXml.Serializer serializer = SettingsXml.serializer(xmlSerializer)) { try (SettingsXml.WriteSection ignored = serializer.startSection( TAG_DOMAIN_VERIFICATIONS)) { @@ -98,26 +101,27 @@ public class DomainVerificationPersistence { } try (SettingsXml.WriteSection activeSection = serializer.startSection(TAG_ACTIVE)) { - writePackageStates(activeSection, active, pkgNameToSignature); + writePackageStates(activeSection, active, userId, pkgNameToSignature); } try (SettingsXml.WriteSection restoredSection = serializer.startSection( TAG_RESTORED)) { - writePackageStates(restoredSection, restored.values(), pkgNameToSignature); + writePackageStates(restoredSection, restored.values(), userId, + pkgNameToSignature); } } } } private static void writePackageStates(@NonNull SettingsXml.WriteSection section, - @NonNull Collection<DomainVerificationPkgState> states, + @NonNull Collection<DomainVerificationPkgState> states, int userId, @Nullable Function<String, String> pkgNameToSignature) throws IOException { if (states.isEmpty()) { return; } for (DomainVerificationPkgState state : states) { - writePkgStateToXml(section, state, pkgNameToSignature); + writePkgStateToXml(section, state, userId, pkgNameToSignature); } } @@ -211,7 +215,7 @@ public class DomainVerificationPersistence { } private static void writePkgStateToXml(@NonNull SettingsXml.WriteSection parentSection, - @NonNull DomainVerificationPkgState pkgState, + @NonNull DomainVerificationPkgState pkgState, @UserIdInt int userId, @Nullable Function<String, String> pkgNameToSignature) throws IOException { String packageName = pkgState.getPackageName(); String signature = pkgNameToSignature == null @@ -231,11 +235,12 @@ public class DomainVerificationPersistence { pkgState.isHasAutoVerifyDomains()) .attribute(ATTR_SIGNATURE, signature)) { writeStateMap(parentSection, pkgState.getStateMap()); - writeUserStates(parentSection, pkgState.getUserStates()); + writeUserStates(parentSection, userId, pkgState.getUserStates()); } } private static void writeUserStates(@NonNull SettingsXml.WriteSection parentSection, + @UserIdInt int userId, @NonNull SparseArray<DomainVerificationInternalUserState> states) throws IOException { int size = states.size(); if (size == 0) { @@ -243,8 +248,15 @@ public class DomainVerificationPersistence { } try (SettingsXml.WriteSection section = parentSection.startSection(TAG_USER_STATES)) { - for (int index = 0; index < size; index++) { - writeUserStateToXml(section, states.valueAt(index)); + if (userId == UserHandle.USER_ALL) { + for (int index = 0; index < size; index++) { + writeUserStateToXml(section, states.valueAt(index)); + } + } else { + DomainVerificationInternalUserState userState = states.get(userId); + if (userState != null) { + writeUserStateToXml(section, userState); + } } } } @@ -278,7 +290,7 @@ public class DomainVerificationPersistence { return null; } - boolean allowLinkHandling = section.getBoolean(ATTR_ALLOW_LINK_HANDLING, true); + boolean allowLinkHandling = section.getBoolean(ATTR_ALLOW_LINK_HANDLING, false); ArraySet<String> enabledHosts = new ArraySet<>(); SettingsXml.ChildSection child = section.children(); diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java index 3a4b849bac3c..f96eeb39e69e 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java @@ -129,10 +129,10 @@ public class DomainVerificationService extends SystemService private final PlatformCompat mPlatformCompat; @NonNull - private final DomainVerificationSettings mSettings; + private final DomainVerificationCollector mCollector; @NonNull - private final DomainVerificationCollector mCollector; + private final DomainVerificationSettings mSettings; @NonNull private final DomainVerificationEnforcer mEnforcer; @@ -159,8 +159,8 @@ public class DomainVerificationService extends SystemService super(context); mSystemConfig = systemConfig; mPlatformCompat = platformCompat; - mSettings = new DomainVerificationSettings(); mCollector = new DomainVerificationCollector(platformCompat, systemConfig); + mSettings = new DomainVerificationSettings(mCollector); mEnforcer = new DomainVerificationEnforcer(context); mDebug = new DomainVerificationDebug(mCollector); mShell = new DomainVerificationShell(this); @@ -1045,21 +1045,29 @@ public class DomainVerificationService extends SystemService } @Override - public void writeSettings(@NonNull TypedXmlSerializer serializer) throws IOException { + public void writeSettings(@NonNull TypedXmlSerializer serializer, boolean includeSignatures, + @UserIdInt int userId) + throws IOException { mConnection.withPackageSettingsThrowing(pkgSettings -> { synchronized (mLock) { - mSettings.writeSettings(serializer, mAttachedPkgStates, pkgName -> { - PackageSetting pkgSetting = pkgSettings.apply(pkgName); - if (pkgSetting == null) { - // If querying for a user restored package that isn't installed on the - // device yet, there will be no signature to write out. In that case, - // it's expected that this returns null and it falls back to the restored - // state's stored signature if it exists. - return null; - } + Function<String, String> pkgNameToSignature = null; + if (includeSignatures) { + pkgNameToSignature = pkgName -> { + PackageSetting pkgSetting = pkgSettings.apply(pkgName); + if (pkgSetting == null) { + // If querying for a user restored package that isn't installed on the + // device yet, there will be no signature to write out. In that case, + // it's expected that this returns null and it falls back to the + // restored state's stored signature if it exists. + return null; + } - return PackageUtils.computeSignaturesSha256Digest(pkgSetting.getSignatures()); - }); + return PackageUtils.computeSignaturesSha256Digest( + pkgSetting.getSignatures()); + }; + } + + mSettings.writeSettings(serializer, mAttachedPkgStates, userId, pkgNameToSignature); } }); @@ -1067,11 +1075,14 @@ public class DomainVerificationService extends SystemService } @Override - public void readSettings(@NonNull TypedXmlPullParser parser) - throws IOException, XmlPullParserException { - synchronized (mLock) { - mSettings.readSettings(parser, mAttachedPkgStates); - } + public void readSettings(@NonNull TypedXmlPullParser parser) throws IOException, + XmlPullParserException { + mConnection.<IOException, XmlPullParserException>withPackageSettingsThrowing2( + pkgSettings -> { + synchronized (mLock) { + mSettings.readSettings(parser, mAttachedPkgStates, pkgSettings); + } + }); } @Override @@ -1083,9 +1094,12 @@ public class DomainVerificationService extends SystemService @Override public void restoreSettings(@NonNull TypedXmlPullParser parser) throws IOException, XmlPullParserException { - synchronized (mLock) { - mSettings.restoreSettings(parser, mAttachedPkgStates); - } + mConnection.<IOException, XmlPullParserException>withPackageSettingsThrowing2( + pkgSettings -> { + synchronized (mLock) { + mSettings.restoreSettings(parser, mAttachedPkgStates, pkgSettings); + } + }); } @Override @@ -2031,6 +2045,15 @@ public class DomainVerificationService extends SystemService } @Override + public <ExceptionOne extends Exception, ExceptionTwo extends Exception> void + withPackageSettingsThrowing2( + @NonNull Throwing2Consumer<Function<String, PackageSetting>, ExceptionOne, + ExceptionTwo> block) throws ExceptionOne, ExceptionTwo { + enforceLocking(); + mConnection.withPackageSettingsThrowing2(block); + } + + @Override public <Output, ExceptionType extends Exception> Output withPackageSettingsReturningThrowing(@NonNull ThrowingFunction<Function<String, PackageSetting>, Output, ExceptionType> block) throws ExceptionType { diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java index c8e95b585b7a..3b2990e33963 100644 --- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java +++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.pm.verify.domain.DomainVerificationState; -import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.SparseArray; @@ -29,6 +28,8 @@ import android.util.TypedXmlSerializer; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.pm.PackageSetting; +import com.android.server.pm.parsing.pkg.AndroidPackage; import com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState; import com.android.server.pm.verify.domain.models.DomainVerificationPkgState; import com.android.server.pm.verify.domain.models.DomainVerificationStateMap; @@ -36,10 +37,15 @@ import com.android.server.pm.verify.domain.models.DomainVerificationStateMap; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.util.Collections; +import java.util.Set; import java.util.function.Function; class DomainVerificationSettings { + @NonNull + private final DomainVerificationCollector mCollector; + /** * States read from disk that have yet to attach to a package, but are expected to, generally in * the context of scanning packages already on disk. This is expected to be empty once the boot @@ -67,24 +73,35 @@ class DomainVerificationSettings { */ private final Object mLock = new Object(); + public DomainVerificationSettings(@NonNull DomainVerificationCollector collector) { + mCollector = collector; + } + + public void writeSettings(@NonNull TypedXmlSerializer xmlSerializer, + @NonNull DomainVerificationStateMap<DomainVerificationPkgState> liveState, + @NonNull Function<String, String> pkgSignatureFunction) { + + } public void writeSettings(@NonNull TypedXmlSerializer xmlSerializer, @NonNull DomainVerificationStateMap<DomainVerificationPkgState> liveState, - @NonNull Function<String, String> pkgSignatureFunction) throws IOException { + @UserIdInt int userId, @NonNull Function<String, String> pkgSignatureFunction) + throws IOException { synchronized (mLock) { DomainVerificationPersistence.writeToXml(xmlSerializer, liveState, - mPendingPkgStates, mRestoredPkgStates, pkgSignatureFunction); + mPendingPkgStates, mRestoredPkgStates, userId, pkgSignatureFunction); } } /** * Parses a previously stored set of states and merges them with {@param liveState}, directly * mutating the values. This is intended for reading settings written by {@link - * #writeSettings(TypedXmlSerializer, DomainVerificationStateMap, Function)} on the same device - * setup. + * #writeSettings(TypedXmlSerializer, DomainVerificationStateMap, int, Function)} on the same + * device setup. */ public void readSettings(@NonNull TypedXmlPullParser parser, - @NonNull DomainVerificationStateMap<DomainVerificationPkgState> liveState) + @NonNull DomainVerificationStateMap<DomainVerificationPkgState> liveState, + @NonNull Function<String, PackageSetting> pkgSettingFunction) throws IOException, XmlPullParserException { DomainVerificationPersistence.ReadResult result = DomainVerificationPersistence.readFromXml(parser); @@ -101,7 +118,7 @@ class DomainVerificationSettings { // This branch should never be possible. Settings should be read from disk // before any states are attached. But just in case, handle it. if (!existingState.getId().equals(pkgState.getId())) { - mergePkgState(existingState, pkgState); + mergePkgState(existingState, pkgState, pkgSettingFunction); } } else { mPendingPkgStates.put(pkgName, pkgState); @@ -121,7 +138,8 @@ class DomainVerificationSettings { * mutating the values. This is intended for restoration across device setups. */ public void restoreSettings(@NonNull TypedXmlPullParser parser, - @NonNull DomainVerificationStateMap<DomainVerificationPkgState> liveState) + @NonNull DomainVerificationStateMap<DomainVerificationPkgState> liveState, + @NonNull Function<String, PackageSetting> pkgSettingFunction) throws IOException, XmlPullParserException { // TODO(b/170746586): Restoration assumes user IDs match, which is probably not the case on // a new device. @@ -148,7 +166,7 @@ class DomainVerificationSettings { } if (existingState != null) { - mergePkgState(existingState, newState); + mergePkgState(existingState, newState, pkgSettingFunction); } else { // If there's no existing state, that means the new state has to be transformed // in preparation for attaching to brand new package that may eventually be @@ -190,31 +208,34 @@ class DomainVerificationSettings { * specific error codes are fresher than the restored state. Essentially state is only restored * to grant additional verifications to an app. * <p> - * For user selection state, presence in either state will be considered an enabled host. NOTE: - * only {@link UserHandle#USER_SYSTEM} is merged. There is no restore path in place for - * multiple users. - * <p> - * TODO(b/170746586): Figure out the restore path for multiple users - * <p> - * This will mutate {@param oldState} to contain the merged state. + * For user selection state, presence in either state will be considered an enabled host. This + * assumes that all user IDs on the device match. If this isn't the case, then restore may set + * unexpected values. + * + * NOTE: This will mutate {@param oldState} to contain the merged state. */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) - public static void mergePkgState(@NonNull DomainVerificationPkgState oldState, - @NonNull DomainVerificationPkgState newState) { + public void mergePkgState(@NonNull DomainVerificationPkgState oldState, + @NonNull DomainVerificationPkgState newState, + @NonNull Function<String, PackageSetting> pkgSettingFunction) { + PackageSetting pkgSetting = pkgSettingFunction.apply(oldState.getPackageName()); + AndroidPackage pkg = pkgSetting == null ? null : pkgSetting.getPkg(); + Set<String> validDomains = pkg == null + ? Collections.emptySet() : mCollector.collectValidAutoVerifyDomains(pkg); + ArrayMap<String, Integer> oldStateMap = oldState.getStateMap(); ArrayMap<String, Integer> newStateMap = newState.getStateMap(); int size = newStateMap.size(); for (int index = 0; index < size; index++) { String domain = newStateMap.keyAt(index); Integer newStateCode = newStateMap.valueAt(index); - Integer oldStateCodeInteger = oldStateMap.get(domain); - if (oldStateCodeInteger == null) { + if (!validDomains.contains(domain)) { // Cannot add domains to an app continue; } - int oldStateCode = oldStateCodeInteger; - if (oldStateCode == DomainVerificationState.STATE_NO_RESPONSE) { + Integer oldStateCode = oldStateMap.get(domain); + if (oldStateCode == null || oldStateCode == DomainVerificationState.STATE_NO_RESPONSE) { if (newStateCode == DomainVerificationState.STATE_SUCCESS || newStateCode == DomainVerificationState.STATE_RESTORED) { oldStateMap.put(domain, DomainVerificationState.STATE_RESTORED); @@ -228,21 +249,21 @@ class DomainVerificationSettings { SparseArray<DomainVerificationInternalUserState> newSelectionStates = newState.getUserStates(); - DomainVerificationInternalUserState newUserState = - newSelectionStates.get(UserHandle.USER_SYSTEM); - if (newUserState != null) { - ArraySet<String> newEnabledHosts = newUserState.getEnabledHosts(); - DomainVerificationInternalUserState oldUserState = - oldSelectionStates.get(UserHandle.USER_SYSTEM); - - boolean linkHandlingAllowed = newUserState.isLinkHandlingAllowed(); - if (oldUserState == null) { - oldUserState = new DomainVerificationInternalUserState(UserHandle.USER_SYSTEM, - newEnabledHosts, linkHandlingAllowed); - oldSelectionStates.put(UserHandle.USER_SYSTEM, oldUserState); - } else { - oldUserState.addHosts(newEnabledHosts) - .setLinkHandlingAllowed(linkHandlingAllowed); + final int userStateSize = newSelectionStates.size(); + for (int index = 0; index < userStateSize; index++) { + int userId = newSelectionStates.keyAt(index); + DomainVerificationInternalUserState newUserState = newSelectionStates.valueAt(index); + if (newUserState != null) { + ArraySet<String> newEnabledHosts = newUserState.getEnabledHosts(); + DomainVerificationInternalUserState oldUserState = oldSelectionStates.get(userId); + boolean linkHandlingAllowed = newUserState.isLinkHandlingAllowed(); + if (oldUserState == null) { + oldSelectionStates.put(userId, new DomainVerificationInternalUserState(userId, + newEnabledHosts, linkHandlingAllowed)); + } else { + oldUserState.addHosts(newEnabledHosts) + .setLinkHandlingAllowed(linkHandlingAllowed); + } } } } diff --git a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxy.java b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxy.java index 09abdd092648..7333d2394b2f 100644 --- a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxy.java +++ b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxy.java @@ -31,7 +31,6 @@ import com.android.server.pm.verify.domain.DomainVerificationMessageCodes; import java.util.Objects; import java.util.Set; -// TODO(b/170321181): Combine the proxy versions for supporting v1 and v2 at once public interface DomainVerificationProxy { String TAG = "DomainVerificationProxy"; @@ -81,8 +80,7 @@ public interface DomainVerificationProxy { return new DomainVerificationProxyUnavailable(); } - default void sendBroadcastForPackages(@NonNull Set<String> packageNames) { - } + void sendBroadcastForPackages(@NonNull Set<String> packageNames); /** * Runs a message on the caller's Handler as a result of {@link BaseConnection#schedule(int, @@ -94,18 +92,12 @@ public interface DomainVerificationProxy { * @param messageCode One of the values in {@link DomainVerificationMessageCodes}. * @param object Arbitrary object that was originally included. */ - default boolean runMessage(int messageCode, Object object) { - return false; - } + boolean runMessage(int messageCode, Object object); - default boolean isCallerVerifier(int callingUid) { - return false; - } + boolean isCallerVerifier(int callingUid); @Nullable - default ComponentName getComponentName() { - return null; - } + ComponentName getComponentName(); interface BaseConnection { diff --git a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyCombined.java b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyCombined.java index 8571c08699bd..5732d6b168fb 100644 --- a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyCombined.java +++ b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyCombined.java @@ -17,6 +17,7 @@ package com.android.server.pm.verify.domain.proxy; import android.annotation.NonNull; +import android.content.ComponentName; import java.util.Set; @@ -51,4 +52,10 @@ class DomainVerificationProxyCombined implements DomainVerificationProxy { public boolean isCallerVerifier(int callingUid) { return mProxyV2.isCallerVerifier(callingUid) || mProxyV1.isCallerVerifier(callingUid); } + + @NonNull + @Override + public ComponentName getComponentName() { + return mProxyV2.getComponentName(); + } } diff --git a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyUnavailable.java b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyUnavailable.java index bd77983256c5..363f9697633c 100644 --- a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyUnavailable.java +++ b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyUnavailable.java @@ -16,6 +16,32 @@ package com.android.server.pm.verify.domain.proxy; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; + +import java.util.Set; + /** Stub implementation for when the verification agent is unavailable */ public class DomainVerificationProxyUnavailable implements DomainVerificationProxy { + + @Override + public void sendBroadcastForPackages(@NonNull Set<String> packageNames) { + } + + @Override + public boolean runMessage(int messageCode, Object object) { + return false; + } + + @Override + public boolean isCallerVerifier(int callingUid) { + return false; + } + + @Nullable + @Override + public ComponentName getComponentName() { + return null; + } } diff --git a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java index fa36683e4aff..c8e46b62f6b0 100644 --- a/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java +++ b/services/core/java/com/android/server/pm/verify/domain/proxy/DomainVerificationProxyV1.java @@ -298,6 +298,12 @@ public class DomainVerificationProxyV1 implements DomainVerificationProxy { return builder.toString(); } + @NonNull + @Override + public ComponentName getComponentName() { + return mVerifierComponent; + } + private static class Response { public final int callingUid; public final int verificationId; diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 29496b31b4a5..0ce9650c76b6 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3021,7 +3021,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { private int handleStartTransitionForKeyguardLw(boolean keyguardGoingAway, long duration) { final int res = applyKeyguardOcclusionChange(); if (res != 0) return res; - if (!WindowManagerService.sEnableRemoteKeyguardAnimation && keyguardGoingAway) { + if (!WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation && keyguardGoingAway) { if (DEBUG_KEYGUARD) Slog.d(TAG, "Starting keyguard exit animation"); startKeyguardExitAnimation(SystemClock.uptimeMillis(), duration); } @@ -5153,7 +5153,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { .compose(); } // fallback for devices without composition support - return VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); + return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK); default: return null; diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java index 44f14b4d5b0d..6e478ee7bf1b 100644 --- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java +++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java @@ -263,7 +263,8 @@ public class KeyguardServiceDelegate { */ @Deprecated public void setOccluded(boolean isOccluded, boolean animate) { - if (!WindowManagerService.sEnableRemoteKeyguardAnimation && mKeyguardService != null) { + if (!WindowManagerService.sEnableRemoteKeyguardOccludeAnimation + && mKeyguardService != null) { if (DEBUG) Log.v(TAG, "setOccluded(" + isOccluded + ") animate=" + animate); mKeyguardService.setOccluded(isOccluded, animate); } @@ -403,7 +404,8 @@ public class KeyguardServiceDelegate { } public void startKeyguardExitAnimation(long startTime, long fadeoutDuration) { - if (!WindowManagerService.sEnableRemoteKeyguardAnimation && mKeyguardService != null) { + if (!WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation + && mKeyguardService != null) { mKeyguardService.startKeyguardExitAnimation(startTime, fadeoutDuration); } } diff --git a/services/core/java/com/android/server/power/DisplayGroupPowerStateMapper.java b/services/core/java/com/android/server/power/DisplayGroupPowerStateMapper.java index ea933246e3b2..52d92703c22f 100644 --- a/services/core/java/com/android/server/power/DisplayGroupPowerStateMapper.java +++ b/services/core/java/com/android/server/power/DisplayGroupPowerStateMapper.java @@ -141,6 +141,14 @@ public class DisplayGroupPowerStateMapper { return mDisplayGroupInfos.get(groupId).lastPowerOnTime; } + void setPoweringOnLocked(int groupId, boolean poweringOn) { + mDisplayGroupInfos.get(groupId).poweringOn = poweringOn; + } + + boolean isPoweringOnLocked(int groupId) { + return mDisplayGroupInfos.get(groupId).poweringOn; + } + /** * Returns the amalgamated wakefulness of all {@link DisplayGroup DisplayGroups}. * @@ -300,6 +308,7 @@ public class DisplayGroupPowerStateMapper { public int wakefulness; public boolean ready; public long lastPowerOnTime; + boolean poweringOn; public boolean sandmanSummoned; public long lastUserActivityTime; public long lastUserActivityTimeNoChangeLights; diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index c4aca6c18453..8991981e6e6d 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1831,6 +1831,7 @@ public final class PowerManagerService extends SystemService setWakefulnessLocked(groupId, WAKEFULNESS_AWAKE, eventTime, uid, reason, opUid, opPackageName, details); mDisplayGroupPowerStateMapper.setLastPowerOnTimeLocked(groupId, eventTime); + mDisplayGroupPowerStateMapper.setPoweringOnLocked(groupId, true); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } @@ -3201,9 +3202,12 @@ public final class PowerManagerService extends SystemService final boolean displayReadyStateChanged = mDisplayGroupPowerStateMapper.setDisplayGroupReadyLocked(groupId, ready); - if (ready && displayReadyStateChanged + final boolean poweringOn = + mDisplayGroupPowerStateMapper.isPoweringOnLocked(groupId); + if (ready && displayReadyStateChanged && poweringOn && mDisplayGroupPowerStateMapper.getWakefulnessLocked( groupId) == WAKEFULNESS_AWAKE) { + mDisplayGroupPowerStateMapper.setPoweringOnLocked(groupId, false); Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, TRACE_SCREEN_ON, groupId); final int latencyMs = (int) (mClock.uptimeMillis() - mDisplayGroupPowerStateMapper.getLastPowerOnTimeLocked(groupId)); diff --git a/services/core/java/com/android/server/utils/OWNERS b/services/core/java/com/android/server/utils/OWNERS new file mode 100644 index 000000000000..cc41f617ab8c --- /dev/null +++ b/services/core/java/com/android/server/utils/OWNERS @@ -0,0 +1,10 @@ +per-file Snappable.java = file:/services/core/java/com/android/server/pm/OWNERS +per-file Snappable.java = shombert@google.com +per-file SnapShot* = file:/services/core/java/com/android/server/pm/OWNERS +per-file SnapShot* = shombert@google.com +per-file Watchable* = file:/services/core/java/com/android/server/pm/OWNERS +per-file Watchable* = shombert@google.com +per-file Watched* = file:/services/core/java/com/android/server/pm/OWNERS +per-file Watched* = shombert@google.com +per-file Watcher.java = file:/services/core/java/com/android/server/pm/OWNERS +per-file Watcher.java = shombert@google.com diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index dfc677278847..bd1d4568db5b 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -421,18 +421,41 @@ public class WindowManagerService extends IWindowManager.Stub SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); /** + * Use WMShell for app transition. + */ + public static final String ENABLE_SHELL_TRANSITIONS = "persist.debug.shell_transit"; + + /** + * @see #ENABLE_SHELL_TRANSITIONS + */ + public static final boolean sEnableShellTransitions = + SystemProperties.getBoolean(ENABLE_SHELL_TRANSITIONS, false); + + /** * Run Keyguard animation as remote animation in System UI instead of local animation in * the server process. + * + * 0: Runs all keyguard animation as local animation + * 1: Only runs keyguard going away animation as remote animation + * 2: Runs all keyguard animation as remote animation */ private static final String ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY = "persist.wm.enable_remote_keyguard_animation"; + private static final int sEnableRemoteKeyguardAnimation = + SystemProperties.getInt(ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY, 1); + /** * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY */ - public static boolean sEnableRemoteKeyguardAnimation = - SystemProperties.getBoolean(ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY, false); + public static final boolean sEnableRemoteKeyguardGoingAwayAnimation = !sEnableShellTransitions + && sEnableRemoteKeyguardAnimation >= 1; + /** + * @see #ENABLE_REMOTE_KEYGUARD_ANIMATION_PROPERTY + */ + public static final boolean sEnableRemoteKeyguardOccludeAnimation = !sEnableShellTransitions + && sEnableRemoteKeyguardAnimation >= 2; /** * Allows a fullscreen windowing mode activity to launch in its desired orientation directly diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 24f73c3ac54c..074eeb942377 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -63,7 +63,7 @@ import static android.app.admin.DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLE import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME; import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS; import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW; -import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_DISABLED; +import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; import static android.app.admin.DevicePolicyManager.NON_ORG_OWNED_PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; import static android.app.admin.DevicePolicyManager.OPERATION_SAFETY_REASON_NONE; import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; @@ -7548,21 +7548,25 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @Override - public int getNearbyNotificationStreamingPolicy() { + public int getNearbyNotificationStreamingPolicy(final int userId) { if (!mHasFeature) { - return NEARBY_STREAMING_DISABLED; + return NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; } final CallerIdentity caller = getCallerIdentity(); Preconditions.checkCallAuthorization( - isDeviceOwner(caller) - || isProfileOwner(caller) - || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY)); + isProfileOwner(caller) + || isDeviceOwner(caller) + || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY)); synchronized (getLockObject()) { - final ActiveAdmin admin = getProfileOwnerOrDeviceOwnerLocked(caller); - return admin.mNearbyNotificationStreamingPolicy; + if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) { + final ActiveAdmin admin = getDeviceOrProfileOwnerAdminLocked(userId); + return admin.mNearbyNotificationStreamingPolicy; + } } + + return NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; } @Override @@ -7584,21 +7588,25 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @Override - public int getNearbyAppStreamingPolicy() { + public int getNearbyAppStreamingPolicy(final int userId) { if (!mHasFeature) { - return NEARBY_STREAMING_DISABLED; + return NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; } final CallerIdentity caller = getCallerIdentity(); Preconditions.checkCallAuthorization( - isDeviceOwner(caller) - || isProfileOwner(caller) - || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY)); + isProfileOwner(caller) + || isDeviceOwner(caller) + || hasCallingOrSelfPermission(permission.READ_NEARBY_STREAMING_POLICY)); synchronized (getLockObject()) { - final ActiveAdmin admin = getProfileOwnerOrDeviceOwnerLocked(caller); - return admin.mNearbyAppStreamingPolicy; + if (mOwners.hasProfileOwner(userId) || mOwners.hasDeviceOwner()) { + final ActiveAdmin admin = getDeviceOrProfileOwnerAdminLocked(userId); + return admin.mNearbyAppStreamingPolicy; + } } + + return NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; } /** diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt index 8540b8afdd64..83c126842213 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPackageTest.kt @@ -48,6 +48,8 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.anyString +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.util.UUID class DomainVerificationPackageTest { @@ -513,12 +515,141 @@ class DomainVerificationPackageTest { assertThat(service.queryValidVerificationPackageNames()).containsExactly(pkgName) } + @Test + fun backupAndRestore() { + // This test acts as a proxy for true user restore through PackageManager, + // as that's much harder to test for real. + + val pkg1 = mockPkgSetting(PKG_ONE, UUID_ONE, SIGNATURE_ONE, listOf(DOMAIN_1, DOMAIN_2)) + val pkg2 = mockPkgSetting(PKG_TWO, UUID_TWO, SIGNATURE_TWO, + listOf(DOMAIN_1, DOMAIN_2, DOMAIN_3)) + val serviceBefore = makeService(pkg1, pkg2) + serviceBefore.addPackage(pkg1) + serviceBefore.addPackage(pkg2) + + serviceBefore.setStatus(pkg1.domainSetId, setOf(DOMAIN_1), STATE_SUCCESS) + serviceBefore.setDomainVerificationLinkHandlingAllowed(pkg1.getName(), false, 1) + serviceBefore.setUserSelection(pkg2.domainSetId, setOf(DOMAIN_2), true, 0) + serviceBefore.setUserSelection(pkg2.domainSetId, setOf(DOMAIN_3), true, 1) + + fun assertExpectedState(service: DomainVerificationService) { + service.assertState( + pkg1, userId = 0, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_NONE, + ) + ) + + service.assertState( + pkg1, userId = 1, linkHandingAllowed = false, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_NONE, + ) + ) + + service.assertState( + pkg2, userId = 0, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_SELECTED, + DOMAIN_3 to DOMAIN_STATE_NONE + ) + ) + + service.assertState( + pkg2, userId = 1, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_NONE, + DOMAIN_3 to DOMAIN_STATE_SELECTED, + ) + ) + } + + assertExpectedState(serviceBefore) + + val backupUser0 = ByteArrayOutputStream().use { + serviceBefore.writeSettings(Xml.resolveSerializer(it), true, 0) + it.toByteArray() + } + + val backupUser1 = ByteArrayOutputStream().use { + serviceBefore.writeSettings(Xml.resolveSerializer(it), true, 1) + it.toByteArray() + } + + val serviceAfter = makeService(pkg1, pkg2) + serviceAfter.addPackage(pkg1) + serviceAfter.addPackage(pkg2) + + // Check the state is default before the restoration applies + listOf(0, 1).forEach { + serviceAfter.assertState( + pkg1, userId = it, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_NONE, + ) + ) + } + + listOf(0, 1).forEach { + serviceAfter.assertState( + pkg2, userId = it, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_NONE, + DOMAIN_3 to DOMAIN_STATE_NONE, + ) + ) + } + + ByteArrayInputStream(backupUser1).use { + serviceAfter.restoreSettings(Xml.resolvePullParser(it)) + } + + // Assert user 1 was restored + serviceAfter.assertState( + pkg1, userId = 1, linkHandingAllowed = false, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_NONE, + ) + ) + + serviceAfter.assertState( + pkg2, userId = 1, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_NONE, + DOMAIN_3 to DOMAIN_STATE_SELECTED, + ) + ) + + // User 0 has domain verified (since that's not user-specific) + serviceAfter.assertState( + pkg1, userId = 0, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_VERIFIED, + DOMAIN_2 to DOMAIN_STATE_NONE, + ) + ) + + // But user 0 is missing any user selected state + serviceAfter.assertState( + pkg2, userId = 0, hostToStateMap = mapOf( + DOMAIN_1 to DOMAIN_STATE_NONE, + DOMAIN_2 to DOMAIN_STATE_NONE, + DOMAIN_3 to DOMAIN_STATE_NONE, + ) + ) + + ByteArrayInputStream(backupUser0).use { + serviceAfter.restoreSettings(Xml.resolvePullParser(it)) + } + + assertExpectedState(serviceAfter) + } + private fun DomainVerificationService.getInfo(pkgName: String) = getDomainVerificationInfo(pkgName) .also { assertThat(it).isNotNull() }!! - private fun DomainVerificationService.getUserState(pkgName: String) = - getDomainVerificationUserState(pkgName, USER_ID) + private fun DomainVerificationService.getUserState(pkgName: String, userId: Int = USER_ID) = + getDomainVerificationUserState(pkgName, userId) .also { assertThat(it).isNotNull() }!! private fun makeService( @@ -600,8 +731,24 @@ class DomainVerificationPackageTest { whenever(this.domainSetId) { domainSetId } whenever(getInstantApp(anyInt())) { false } whenever(firstInstallTime) { 0L } - whenever(readUserState(USER_ID)) { PackageUserState() } + whenever(readUserState(0)) { PackageUserState() } + whenever(readUserState(1)) { PackageUserState() } whenever(signatures) { arrayOf(Signature(signature)) } whenever(isSystem) { isSystemApp } } + + private fun DomainVerificationService.assertState( + pkg: PackageSetting, + userId: Int, + linkHandingAllowed: Boolean = true, + hostToStateMap: Map<String, Int> + ) { + getUserState(pkg.getName(), userId).apply { + assertThat(this.packageName).isEqualTo(pkg.getName()) + assertThat(this.identifier).isEqualTo(pkg.domainSetId) + assertThat(this.isLinkHandlingAllowed).isEqualTo(linkHandingAllowed) + assertThat(this.user.identifier).isEqualTo(userId) + assertThat(this.hostToStateMap).containsExactlyEntriesIn(hostToStateMap) + } + } } diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt index 7ffbbf6af159..ad652dff901c 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt @@ -17,6 +17,7 @@ package com.android.server.pm.test.verify.domain import android.content.pm.verify.domain.DomainVerificationState +import android.os.UserHandle import android.util.ArrayMap import android.util.SparseArray import android.util.TypedXmlPullParser @@ -110,7 +111,8 @@ class DomainVerificationPersistenceTest { fun writeAndReadBackNormal() { val (attached, pending, restored) = mockWriteValues() val file = tempFolder.newFile().writeXml { - DomainVerificationPersistence.writeToXml(it, attached, pending, restored, null) + DomainVerificationPersistence.writeToXml(it, attached, pending, restored, + UserHandle.USER_ALL, null) } val xml = file.readText() @@ -128,7 +130,8 @@ class DomainVerificationPersistenceTest { fun writeAndReadBackWithSignature() { val (attached, pending, restored) = mockWriteValues() val file = tempFolder.newFile().writeXml { - DomainVerificationPersistence.writeToXml(it, attached, pending, restored) { + DomainVerificationPersistence.writeToXml(it, attached, pending, restored, + UserHandle.USER_ALL) { "SIGNATURE_$it" } } @@ -156,7 +159,8 @@ class DomainVerificationPersistenceTest { fun writeStateSignatureIfFunctionReturnsNull() { val (attached, pending, restored) = mockWriteValues { "SIGNATURE_$it" } val file = tempFolder.newFile().writeXml { - DomainVerificationPersistence.writeToXml(it, attached, pending, restored) { null } + DomainVerificationPersistence.writeToXml(it, attached, pending, restored, + UserHandle.USER_ALL) { null } } val (readActive, readRestored) = file.readXml { @@ -254,7 +258,7 @@ class DomainVerificationPersistenceTest { > <state/> <user-states> - <user-state userId="1" allowLinkHandling="false"> + <user-state userId="1"> <enabled-hosts> <host name="example-user1.com"/> <host name="example-user1.org"/> diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationProxyTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationProxyTest.kt index a9b77ea6d95b..0a54094b4b85 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationProxyTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationProxyTest.kt @@ -466,6 +466,44 @@ class DomainVerificationProxyTest { } } + @Test + fun nonNullComponentName() { + val connection = mockConnection() + DomainVerificationProxy.makeProxy<Connection>( + componentTwo, + null, + context, + manager, + collector, + connection + ).run { + assertThat(componentName).isEqualTo(componentTwo) + } + + DomainVerificationProxy.makeProxy<Connection>( + null, + componentThree, + context, + manager, + collector, + connection + ).run { + assertThat(componentName).isEqualTo(componentThree) + } + + DomainVerificationProxy.makeProxy<Connection>( + componentTwo, + componentThree, + context, + manager, + collector, + connection + ).run { + // Higher version takes precedence + assertThat(componentName).isEqualTo(componentThree) + } + } + private fun mockConnection(block: Connection.() -> Unit = {}) = mockThrowOnUnmocked<Connection> { whenever(isCallerPackage(TEST_CALLING_UID_ACCEPT, TEST_PKG_NAME_ONE)) { true } diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationTestUtils.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationTestUtils.kt index e1da727fb64f..00986f741eea 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationTestUtils.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationTestUtils.kt @@ -42,6 +42,11 @@ internal object DomainVerificationTestUtils { Function<String, PackageSetting?>, *>) .accept { block(it) } } + whenever(withPackageSettingsThrowing2<Exception, Exception>(any())) { + (arguments[0] as DomainVerificationManagerInternal.Connection.Throwing2Consumer< + Function<String, PackageSetting?>, *, *>) + .accept { block(it) } + } whenever(withPackageSettingsReturningThrowing<Any, Exception>(any())) { (arguments[0] as DomainVerificationManagerInternal.Connection.ThrowingFunction< Function<String, PackageSetting?>, *, *>) diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java index f1d8e6c167d7..e1012a9a4cad 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -649,14 +649,14 @@ public class LocalDisplayAdapterTest { // Test as default display BacklightAdapter ba = new BacklightAdapter(displayToken, true /*isDefault*/, mSurfaceControlProxy); - ba.setBacklight(0.514f); - verify(mSurfaceControlProxy).setDisplayBrightness(displayToken, 0.514f); + ba.setBacklight(0.514f, 100f, 0.614f, 500f); + verify(mSurfaceControlProxy).setDisplayBrightness(displayToken, 0.514f, 100f, 0.614f, 500f); // Test as not default display BacklightAdapter ba2 = new BacklightAdapter(displayToken, false /*isDefault*/, mSurfaceControlProxy); - ba2.setBacklight(0.323f); - verify(mSurfaceControlProxy).setDisplayBrightness(displayToken, 0.323f); + ba2.setBacklight(0.323f, 101f, 0.723f, 601f); + verify(mSurfaceControlProxy).setDisplayBrightness(displayToken, 0.323f, 101f, 0.723f, 601f); } @Test @@ -668,7 +668,7 @@ public class LocalDisplayAdapterTest { BacklightAdapter ba = new BacklightAdapter(displayToken, true /*isDefault*/, mSurfaceControlProxy); - ba.setBacklight(0.123f); + ba.setBacklight(1f, 1f, 0.123f, 1f); verify(mMockedBacklight).setBrightness(0.123f); } @@ -681,7 +681,7 @@ public class LocalDisplayAdapterTest { BacklightAdapter ba = new BacklightAdapter(displayToken, false /*isDefault*/, mSurfaceControlProxy); - ba.setBacklight(0.456f); + ba.setBacklight(0.456f, 1f, 1f, 1f); // Adapter does not forward any brightness in this case. verify(mMockedBacklight, never()).setBrightness(anyFloat()); diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java index 88a21b4a8fa8..8e4cdc91d0e6 100644 --- a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java @@ -21,6 +21,7 @@ import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLI import static org.junit.Assert.assertEquals; +import android.os.Binder; import android.os.Handler; import android.os.Message; import android.os.test.TestLooper; @@ -55,6 +56,7 @@ public class HighBrightnessModeControllerTest { private OffsettableClock mClock; private TestLooper mTestLooper; private Handler mHandler; + private Binder mDisplayToken; private static final HighBrightnessModeData DEFAULT_HBM_DATA = new HighBrightnessModeData(MINIMUM_LUX, TRANSITION_POINT, TIME_WINDOW_MILLIS, @@ -64,6 +66,7 @@ public class HighBrightnessModeControllerTest { public void setUp() { mClock = new OffsettableClock.Stopped(); mTestLooper = new TestLooper(mClock::now); + mDisplayToken = null; mHandler = new Handler(mTestLooper.getLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message msg) { @@ -79,14 +82,14 @@ public class HighBrightnessModeControllerTest { @Test public void testNoHbmData() { final HighBrightnessModeController hbmc = new HighBrightnessModeController( - mClock::now, mHandler, DEFAULT_MIN, DEFAULT_MAX, null, () -> {}); + mClock::now, mHandler, mDisplayToken, DEFAULT_MIN, DEFAULT_MAX, null, () -> {}); assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF); } @Test public void testNoHbmData_Enabled() { final HighBrightnessModeController hbmc = new HighBrightnessModeController( - mClock::now, mHandler, DEFAULT_MIN, DEFAULT_MAX, null, () -> {}); + mClock::now, mHandler, mDisplayToken, DEFAULT_MIN, DEFAULT_MAX, null, () -> {}); hbmc.setAutoBrightnessEnabled(true); hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF); @@ -264,8 +267,8 @@ public class HighBrightnessModeControllerTest { // Creates instance with standard initialization values. private HighBrightnessModeController createDefaultHbm() { - return new HighBrightnessModeController(mClock::now, mHandler, DEFAULT_MIN, DEFAULT_MAX, - DEFAULT_HBM_DATA, () -> {}); + return new HighBrightnessModeController(mClock::now, mHandler, mDisplayToken, DEFAULT_MIN, + DEFAULT_MAX, DEFAULT_HBM_DATA, () -> {}); } private void advanceTime(long timeMs) { diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java index 765c13a98ac0..7b4803724180 100644 --- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java +++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java @@ -857,14 +857,15 @@ public final class UpdatableFontDirTest { mConfigFile, mCurrentTimeSupplier, mConfigSupplier); dir.loadFontFileMap(); + List<FontUpdateRequest> requests = Arrays.asList( + newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE), + newAddFontFamilyRequest("<family lang='en'>" + + " <font>test.ttf</font>" + + "</family>")); try { - dir.update(Arrays.asList( - newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE), - newAddFontFamilyRequest("<family lang='en'>" - + " <font>test.ttf</font>" - + "</family>"))); + dir.update(requests); fail("Expect NullPointerException"); - } catch (FontManagerService.SystemFontException e) { + } catch (NullPointerException e) { // Expect } } diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index 4df469e79814..81c237b8ccac 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -555,12 +555,13 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { public void requestProjection() throws Exception { when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); // Should work for all powers of two. - for (int p = 1; p < PROJECTION_TYPE_ALL; p = p * 2) { - assertTrue(mService.requestProjection(mBinder, p, PACKAGE_NAME)); - assertTrue((mService.getActiveProjectionTypes() & p) != 0); - assertThat(mService.getProjectingPackages(p), contains(PACKAGE_NAME)); + for (int i = 0; i < Integer.SIZE; ++i) { + int projectionType = 1 << i; + assertTrue(mService.requestProjection(mBinder, projectionType, PACKAGE_NAME)); + assertTrue((mService.getActiveProjectionTypes() & projectionType) != 0); + assertThat(mService.getProjectingPackages(projectionType), contains(PACKAGE_NAME)); // Subsequent calls should still succeed. - assertTrue(mService.requestProjection(mBinder, p, PACKAGE_NAME)); + assertTrue(mService.requestProjection(mBinder, projectionType, PACKAGE_NAME)); } assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); } @@ -613,19 +614,17 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { @Test public void releaseProjection() throws Exception { when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); - // Should work for all powers of two. - for (int p = 1; p < PROJECTION_TYPE_ALL; p = p * 2) { - mService.requestProjection(mBinder, p, PACKAGE_NAME); - } + requestAllPossibleProjectionTypes(); assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); assertTrue(mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); int everythingButAutomotive = PROJECTION_TYPE_ALL & ~PROJECTION_TYPE_AUTOMOTIVE; assertEquals(everythingButAutomotive, mService.getActiveProjectionTypes()); - for (int p = 1; p < PROJECTION_TYPE_ALL; p = p * 2) { - assertEquals(p != PROJECTION_TYPE_AUTOMOTIVE, - (boolean) mService.releaseProjection(p, PACKAGE_NAME)); + for (int i = 0; i < Integer.SIZE; ++i) { + int projectionType = 1 << i; + assertEquals(projectionType != PROJECTION_TYPE_AUTOMOTIVE, + (boolean) mService.releaseProjection(projectionType, PACKAGE_NAME)); } assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); @@ -634,9 +633,7 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { @Test public void binderDeath_releasesProjection() throws Exception { when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); - for (int p = 1; p < PROJECTION_TYPE_ALL; p = p * 2) { - mService.requestProjection(mBinder, p, PACKAGE_NAME); - } + requestAllPossibleProjectionTypes(); assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor = ArgumentCaptor.forClass( IBinder.DeathRecipient.class); @@ -814,6 +811,12 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { verify(listener, never()).onProjectionStateChanged(anyInt(), any()); } + private void requestAllPossibleProjectionTypes() throws RemoteException { + for (int i = 0; i < Integer.SIZE; ++i) { + mService.requestProjection(mBinder, 1 << i, PACKAGE_NAME); + } + } + private static class TestInjector extends UiModeManagerService.Injector { private static final int CALLING_UID = 8675309; diff --git a/services/usb/java/com/android/server/usb/UsbService.java b/services/usb/java/com/android/server/usb/UsbService.java index ac6b8fe0b970..3d3538d7ae49 100644 --- a/services/usb/java/com/android/server/usb/UsbService.java +++ b/services/usb/java/com/android/server/usb/UsbService.java @@ -200,7 +200,7 @@ public class UsbService extends IUsbManager.Stub { final IntentFilter filter = new IntentFilter(); filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); - mContext.registerReceiver(receiver, filter, null, null); + mContext.registerReceiverAsUser(receiver, UserHandle.ALL, filter, null, null); } /** diff --git a/telecomm/java/android/telecom/DiagnosticCall.java b/telecomm/java/android/telecom/DiagnosticCall.java deleted file mode 100644 index a6b7258052a4..000000000000 --- a/telecomm/java/android/telecom/DiagnosticCall.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.telecom; - -import android.annotation.SystemApi; - -/** - * @deprecated use {@link CallDiagnostics} instead. - * @hide - */ -@SystemApi -public abstract class DiagnosticCall extends CallDiagnostics { -} diff --git a/telephony/java/android/telephony/CellIdentityNr.java b/telephony/java/android/telephony/CellIdentityNr.java index cdd54cd25311..4f5052151af5 100644 --- a/telephony/java/android/telephony/CellIdentityNr.java +++ b/telephony/java/android/telephony/CellIdentityNr.java @@ -233,7 +233,7 @@ public final class CellIdentityNr extends CellIdentity { } /** - * @return Mobile Network Code in string fomrat, or {@code null} if unknown. + * @return Mobile Network Code in string format, or {@code null} if unknown. */ @Nullable public String getMncString() { |