Merge "[Catalyst] Support Set API" into main
diff --git a/ACTIVITY_SECURITY_OWNERS b/ACTIVITY_SECURITY_OWNERS
new file mode 100644
index 0000000..c39842e
--- /dev/null
+++ b/ACTIVITY_SECURITY_OWNERS
@@ -0,0 +1,2 @@
+haok@google.com
+wnan@google.com
\ No newline at end of file
diff --git a/INTENT_OWNERS b/INTENT_OWNERS
index 58b5f2a..c828215 100644
--- a/INTENT_OWNERS
+++ b/INTENT_OWNERS
@@ -1,3 +1,4 @@
include /PACKAGE_MANAGER_OWNERS
include /services/core/java/com/android/server/wm/OWNERS
include /services/core/java/com/android/server/am/OWNERS
+include /ACTIVITY_SECURITY_OWNERS
\ No newline at end of file
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
index 5f55075..4be16a6 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -30,3 +30,10 @@
description: "Enables automatic cancellation of jobs due to leaked JobParameters, reducing unnecessary battery drain and improving system efficiency. This includes logging and traces for better issue diagnosis."
bug: "349688611"
}
+
+flag {
+ name: "ignore_important_while_foreground"
+ namespace: "backstage_power"
+ description: "Ignore the important_while_foreground flag and change the related APIs to be not effective"
+ bug: "374175032"
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 5f57c39..cc2d104 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -809,9 +809,21 @@
}
/**
+ * <p class="caution"><strong>Note:</strong> Beginning with
+ * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer
+ * function effectively, regardless of the calling app's target SDK version.
+ * Calling this method will always return {@code false}.
+ *
* @see JobInfo.Builder#setImportantWhileForeground(boolean)
+ *
+ * @deprecated Use {@link #isExpedited()} instead.
*/
+ @FlaggedApi(Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND)
+ @Deprecated
public boolean isImportantWhileForeground() {
+ if (Flags.ignoreImportantWhileForeground()) {
+ return false;
+ }
return (flags & FLAG_IMPORTANT_WHILE_FOREGROUND) != 0;
}
@@ -2124,6 +2136,13 @@
* <p>
* Jobs marked as important-while-foreground are given {@link #PRIORITY_HIGH} by default.
*
+ * <p class="caution"><strong>Note:</strong> Beginning with
+ * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer
+ * function effectively, regardless of the calling app's target SDK version.
+ * {link #isImportantWhileForeground()} will always return {@code false}.
+ * Apps should use {link #setExpedited(boolean)} with {@code true} to indicate
+ * that this job is important and needs to run as soon as possible.
+ *
* @param importantWhileForeground whether to relax doze restrictions for this job when the
* app is in the foreground. False by default.
* @see JobInfo#isImportantWhileForeground()
@@ -2131,6 +2150,12 @@
*/
@Deprecated
public Builder setImportantWhileForeground(boolean importantWhileForeground) {
+ if (Flags.ignoreImportantWhileForeground()) {
+ Log.w(TAG, "Requested important-while-foreground flag for job" + mJobId
+ + " is ignored and takes no effect");
+ return this;
+ }
+
if (importantWhileForeground) {
mFlags |= FLAG_IMPORTANT_WHILE_FOREGROUND;
if (mPriority == PRIORITY_DEFAULT) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 97c6e25..4c1951a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -5864,6 +5864,9 @@
pw.print(android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION,
android.app.job.Flags.backupJobsExemption());
pw.println();
+ pw.print(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND,
+ android.app.job.Flags.ignoreImportantWhileForeground());
+ pw.println();
pw.decreaseIndent();
pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
index ac240cc..68303e8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -436,6 +436,9 @@
case android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION:
pw.println(android.app.job.Flags.backupJobsExemption());
break;
+ case android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND:
+ pw.println(android.app.job.Flags.ignoreImportantWhileForeground());
+ break;
default:
pw.println("Unknown flag: " + flagName);
break;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
index d5c9ae6..abec170 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -210,7 +210,9 @@
}
private boolean updateTaskStateLocked(JobStatus task, final long nowElapsed) {
- final boolean allowInIdle = ((task.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0)
+ final boolean allowInIdle =
+ (!android.app.job.Flags.ignoreImportantWhileForeground()
+ && ((task.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0))
&& (mForegroundUids.get(task.getSourceUid()) || isTempWhitelistedLocked(task));
final boolean whitelisted = isWhitelistedLocked(task);
final boolean enableTask = !mDeviceIdleMode || whitelisted || allowInIdle;
@@ -219,7 +221,8 @@
@Override
public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
- if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
+ if (!android.app.job.Flags.ignoreImportantWhileForeground()
+ && (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
mAllowInIdleJobs.add(jobStatus);
}
updateTaskStateLocked(jobStatus, sElapsedRealtimeClock.millis());
@@ -227,7 +230,8 @@
@Override
public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob) {
- if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
+ if (!android.app.job.Flags.ignoreImportantWhileForeground()
+ && (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
mAllowInIdleJobs.remove(jobStatus);
}
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 37e2fe2..ff4af69 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -793,7 +793,9 @@
|| isTopStartedJobLocked(jobStatus)
|| isUidInForeground(jobStatus.getSourceUid());
final boolean isJobImportant = jobStatus.getEffectivePriority() >= JobInfo.PRIORITY_HIGH
- || (jobStatus.getFlags() & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0;
+ || (!android.app.job.Flags.ignoreImportantWhileForeground()
+ && (jobStatus.getFlags()
+ & JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0);
if (isInPrivilegedState && isJobImportant) {
return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
}
diff --git a/core/api/current.txt b/core/api/current.txt
index 94481b6..13e1210 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9113,7 +9113,7 @@
method public long getTriggerContentUpdateDelay();
method @Nullable public android.app.job.JobInfo.TriggerContentUri[] getTriggerContentUris();
method public boolean isExpedited();
- method public boolean isImportantWhileForeground();
+ method @Deprecated @FlaggedApi("android.app.job.ignore_important_while_foreground") public boolean isImportantWhileForeground();
method public boolean isPeriodic();
method public boolean isPersisted();
method public boolean isPrefetch();
@@ -19034,7 +19034,9 @@
method @FlaggedApi("android.hardware.biometrics.last_authentication_time") @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public long getLastAuthenticationTime(int);
method @NonNull @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public android.hardware.biometrics.BiometricManager.Strings getStrings(int);
field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE = 20; // 0x14
field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb
+ field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21; // 0x15
field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc
field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf
field @FlaggedApi("android.hardware.biometrics.last_authentication_time") public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL
@@ -19045,6 +19047,7 @@
field public static final int BIOMETRIC_STRONG = 15; // 0xf
field public static final int BIOMETRIC_WEAK = 255; // 0xff
field public static final int DEVICE_CREDENTIAL = 32768; // 0x8000
+ field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int IDENTITY_CHECK = 65536; // 0x10000
}
public static class BiometricManager.Strings {
@@ -19077,8 +19080,10 @@
field public static final int BIOMETRIC_ERROR_CANCELED = 5; // 0x5
field public static final int BIOMETRIC_ERROR_HW_NOT_PRESENT = 12; // 0xc
field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1
+ field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE = 20; // 0x14
field public static final int BIOMETRIC_ERROR_LOCKOUT = 7; // 0x7
field public static final int BIOMETRIC_ERROR_LOCKOUT_PERMANENT = 9; // 0x9
+ field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21; // 0x15
field public static final int BIOMETRIC_ERROR_NO_BIOMETRICS = 11; // 0xb
field public static final int BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL = 14; // 0xe
field public static final int BIOMETRIC_ERROR_NO_SPACE = 4; // 0x4
@@ -34377,6 +34382,7 @@
method public static android.os.VibrationEffect createWaveform(long[], int[], int);
method public int describeContents();
method @NonNull public static android.os.VibrationEffect.Composition startComposition();
+ method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public static android.os.VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope();
field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR;
field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff
field public static final int EFFECT_CLICK = 0; // 0x0
@@ -34400,6 +34406,11 @@
field public static final int PRIMITIVE_TICK = 7; // 0x7
}
+ @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public static final class VibrationEffect.WaveformEnvelopeBuilder {
+ method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect.WaveformEnvelopeBuilder addControlPoint(@FloatRange(from=0, to=1) float, @FloatRange(from=0) float, int);
+ method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public android.os.VibrationEffect build();
+ }
+
public abstract class Vibrator {
method public final int areAllEffectsSupported(@NonNull int...);
method public final boolean areAllPrimitivesSupported(@NonNull int...);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 79bea01..58ab073 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2771,6 +2771,17 @@
field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PrimitiveSegment> CREATOR;
}
+ @FlaggedApi("android.os.vibrator.normalized_pwle_effects") public final class PwleSegment extends android.os.vibrator.VibrationEffectSegment {
+ method public int describeContents();
+ method public long getDuration();
+ method public float getEndAmplitude();
+ method public float getEndFrequencyHz();
+ method public float getStartAmplitude();
+ method public float getStartFrequencyHz();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.os.vibrator.PwleSegment> CREATOR;
+ }
+
public final class RampSegment extends android.os.vibrator.VibrationEffectSegment {
method public int describeContents();
method public long getDuration();
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index f2a36e9..768b70c 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -39,6 +39,7 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ShortcutInfo;
import android.graphics.drawable.Icon;
@@ -1344,11 +1345,15 @@
*/
@FlaggedApi(Flags.FLAG_MODES_API)
public boolean areAutomaticZenRulesUserManaged() {
- // modes ui is dependent on modes api
- return Flags.modesApi() && Flags.modesUi();
+ if (Flags.modesApi() && Flags.modesUi()) {
+ PackageManager pm = mContext.getPackageManager();
+ return !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ } else {
+ return false;
+ }
}
-
/**
* Returns AutomaticZenRules owned by the caller.
*
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index 3839b5f..e5c94fc 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -55,7 +55,7 @@
name: "remote_views_proto"
namespace: "app_widgets"
description: "Enable support for persisting RemoteViews previews to Protobuf"
- bug: "306546610"
+ bug: "364420494"
}
flag {
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 1d4403c..8e02ffd 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -16,9 +16,11 @@
package android.content.pm;
+import android.Manifest;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -513,6 +515,7 @@
* Note that, internally, this does not perform an exact copy.
* @hide
*/
+ @SuppressLint("MissingPermission")
public UserProperties(UserProperties orig,
boolean exposeAllFields,
boolean hasManagePermission,
@@ -614,12 +617,10 @@
* {@link #SHOW_IN_SETTINGS_SEPARATE},
* and {@link #SHOW_IN_SETTINGS_NO}.
*
- * <p> The caller must have {@link android.Manifest.permission#MANAGE_USERS} to query this
- * property.
- *
* @return whether, and how, a profile should be shown in the Settings.
* @hide
*/
+ @RequiresPermission(Manifest.permission.MANAGE_USERS)
public @ShowInSettings int getShowInSettings() {
if (isPresent(INDEX_SHOW_IN_SETTINGS)) return mShowInSettings;
if (mDefaultProperties != null) return mDefaultProperties.mShowInSettings;
@@ -690,6 +691,8 @@
/**
* Returns whether a profile should be started when its parent starts (unless in quiet mode).
* This only applies for users that have parents (i.e. for profiles).
+ *
+ * Only available to the SYSTEM uid.
* @hide
*/
public boolean getStartWithParent() {
@@ -708,6 +711,8 @@
* Returns whether an app in the profile should be deleted when the same package in
* the parent user is being deleted.
* This only applies for users that have parents (i.e. for profiles).
+ *
+ * Only available to the SYSTEM uid.
* @hide
*/
public boolean getDeleteAppWithParent() {
@@ -726,6 +731,8 @@
* Returns whether the user should always
* be {@link android.os.UserManager#isUserVisible() visible}.
* The intended usage is for the Communal Profile, which is running and accessible at all times.
+ *
+ * Only available to the SYSTEM uid.
* @hide
*/
public boolean getAlwaysVisible() {
@@ -747,6 +754,7 @@
* Possible return values include
* {@link #INHERIT_DEVICE_POLICY_FROM_PARENT} or {@link #INHERIT_DEVICE_POLICY_NO}
*
+ * Only available to the SYSTEM uid.
* @hide
*/
public @InheritDevicePolicy int getInheritDevicePolicy() {
@@ -777,6 +785,7 @@
* @return whether contacts access from an associated profile is enabled for the user
* @hide
*/
+ @RequiresPermission(Manifest.permission.MANAGE_USERS)
public boolean getUseParentsContacts() {
if (isPresent(INDEX_USE_PARENTS_CONTACTS)) return mUseParentsContacts;
if (mDefaultProperties != null) return mDefaultProperties.mUseParentsContacts;
@@ -796,7 +805,9 @@
/**
* Returns true if user needs to update default
- * {@link com.android.server.pm.CrossProfileIntentFilter} with its parents during an OTA update
+ * {@link com.android.server.pm.CrossProfileIntentFilter} with its parents during an OTA update.
+ *
+ * Only available to the SYSTEM uid.
* @hide
*/
public boolean getUpdateCrossProfileIntentFiltersOnOTA() {
@@ -863,6 +874,7 @@
* checks is not guaranteed when the property is false and may vary depending on user types.
* @hide
*/
+ @RequiresPermission(Manifest.permission.MANAGE_USERS)
public boolean isAuthAlwaysRequiredToDisableQuietMode() {
if (isPresent(INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE)) {
return mAuthAlwaysRequiredToDisableQuietMode;
@@ -894,6 +906,8 @@
* locking for a user can happen if either the device configuration is set or if this property
* is set. When both, the config and the property value is false, the user storage is always
* locked when the user is stopped.
+ *
+ * Only available to the SYSTEM uid.
* @hide
*/
public boolean getAllowStoppingUserWithDelayedLocking() {
@@ -915,6 +929,8 @@
/**
* Returns the user's {@link CrossProfileIntentFilterAccessControlLevel}.
+ *
+ * Only available to the SYSTEM uid.
* @hide
*/
public @CrossProfileIntentFilterAccessControlLevel int
@@ -944,6 +960,7 @@
* Returns the user's {@link CrossProfileIntentResolutionStrategy}.
* @return user's {@link CrossProfileIntentResolutionStrategy}.
*
+ * Only available to the SYSTEM uid.
* @hide
*/
public @CrossProfileIntentResolutionStrategy int getCrossProfileIntentResolutionStrategy() {
@@ -1052,32 +1069,47 @@
@Override
public String toString() {
+ StringBuilder s = new StringBuilder();
+ s.append("UserProperties{");
+ s.append("mPropertiesPresent="); s.append(Long.toBinaryString(mPropertiesPresent));
+ try {
+ s.append(listPropertiesAsStringBuilder());
+ } catch (SecurityException e) {
+ // Caller doesn't have permission to see all the properties. Just don't share them.
+ }
+ s.append("}");
+ return s.toString();
+ }
+
+ private StringBuilder listPropertiesAsStringBuilder() {
+ final StringBuilder s = new StringBuilder();
+
// Please print in increasing order of PropertyIndex.
- return "UserProperties{"
- + "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent)
- + ", mShowInLauncher=" + getShowInLauncher()
- + ", mStartWithParent=" + getStartWithParent()
- + ", mShowInSettings=" + getShowInSettings()
- + ", mInheritDevicePolicy=" + getInheritDevicePolicy()
- + ", mUseParentsContacts=" + getUseParentsContacts()
- + ", mUpdateCrossProfileIntentFiltersOnOTA="
- + getUpdateCrossProfileIntentFiltersOnOTA()
- + ", mCrossProfileIntentFilterAccessControl="
- + getCrossProfileIntentFilterAccessControl()
- + ", mCrossProfileIntentResolutionStrategy="
- + getCrossProfileIntentResolutionStrategy()
- + ", mMediaSharedWithParent=" + isMediaSharedWithParent()
- + ", mCredentialShareableWithParent=" + isCredentialShareableWithParent()
- + ", mAuthAlwaysRequiredToDisableQuietMode="
- + isAuthAlwaysRequiredToDisableQuietMode()
- + ", mAllowStoppingUserWithDelayedLocking="
- + getAllowStoppingUserWithDelayedLocking()
- + ", mDeleteAppWithParent=" + getDeleteAppWithParent()
- + ", mAlwaysVisible=" + getAlwaysVisible()
- + ", mCrossProfileContentSharingStrategy=" + getCrossProfileContentSharingStrategy()
- + ", mProfileApiVisibility=" + getProfileApiVisibility()
- + ", mItemsRestrictedOnHomeScreen=" + areItemsRestrictedOnHomeScreen()
- + "}";
+ s.append(", mShowInLauncher="); s.append(getShowInLauncher());
+ s.append(", mStartWithParent="); s.append(getStartWithParent());
+ s.append(", mShowInSettings="); s.append(getShowInSettings());
+ s.append(", mInheritDevicePolicy="); s.append(getInheritDevicePolicy());
+ s.append(", mUseParentsContacts="); s.append(getUseParentsContacts());
+ s.append(", mUpdateCrossProfileIntentFiltersOnOTA=");
+ s.append(getUpdateCrossProfileIntentFiltersOnOTA());
+ s.append(", mCrossProfileIntentFilterAccessControl=");
+ s.append(getCrossProfileIntentFilterAccessControl());
+ s.append(", mCrossProfileIntentResolutionStrategy=");
+ s.append(getCrossProfileIntentResolutionStrategy());
+ s.append(", mMediaSharedWithParent="); s.append(isMediaSharedWithParent());
+ s.append(", mCredentialShareableWithParent="); s.append(isCredentialShareableWithParent());
+ s.append(", mAuthAlwaysRequiredToDisableQuietMode=");
+ s.append(isAuthAlwaysRequiredToDisableQuietMode());
+ s.append(", mAllowStoppingUserWithDelayedLocking=");
+ s.append(getAllowStoppingUserWithDelayedLocking());
+ s.append(", mDeleteAppWithParent="); s.append(getDeleteAppWithParent());
+ s.append(", mAlwaysVisible="); s.append(getAlwaysVisible());
+ s.append(", mCrossProfileContentSharingStrategy=");
+ s.append(getCrossProfileContentSharingStrategy());
+ s.append(", mProfileApiVisibility="); s.append(getProfileApiVisibility());
+ s.append(", mItemsRestrictedOnHomeScreen="); s.append(areItemsRestrictedOnHomeScreen());
+
+ return s;
}
/**
diff --git a/core/java/android/database/BulkCursorNative.java b/core/java/android/database/BulkCursorNative.java
index 8ea450c..41585b3 100644
--- a/core/java/android/database/BulkCursorNative.java
+++ b/core/java/android/database/BulkCursorNative.java
@@ -53,7 +53,7 @@
return new BulkCursorProxy(obj);
}
-
+
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
@@ -79,7 +79,7 @@
reply.writeNoException();
return true;
}
-
+
case CLOSE_TRANSACTION: {
data.enforceInterface(IBulkCursor.descriptor);
close();
@@ -212,15 +212,22 @@
Parcel reply = Parcel.obtain();
try {
data.writeInterfaceToken(IBulkCursor.descriptor);
-
- mRemote.transact(CLOSE_TRANSACTION, data, reply, 0);
- DatabaseUtils.readExceptionFromParcel(reply);
+ // If close() is being called from the finalizer thread, do not wait for a reply from
+ // the remote side.
+ final boolean fromFinalizer =
+ android.database.sqlite.Flags.onewayFinalizerClose()
+ && "FinalizerDaemon".equals(Thread.currentThread().getName());
+ mRemote.transact(CLOSE_TRANSACTION, data, reply,
+ fromFinalizer ? IBinder.FLAG_ONEWAY: 0);
+ if (!fromFinalizer) {
+ DatabaseUtils.readExceptionFromParcel(reply);
+ }
} finally {
data.recycle();
reply.recycle();
}
}
-
+
public int requery(IContentObserver observer) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
@@ -282,4 +289,3 @@
}
}
}
-
diff --git a/core/java/android/database/sqlite/flags.aconfig b/core/java/android/database/sqlite/flags.aconfig
index d5a7db8..826b908 100644
--- a/core/java/android/database/sqlite/flags.aconfig
+++ b/core/java/android/database/sqlite/flags.aconfig
@@ -2,6 +2,13 @@
container: "system"
flag {
+ name: "oneway_finalizer_close"
+ namespace: "system_performance"
+ description: "Make BuildCursorNative.close oneway if in the the finalizer"
+ bug: "368221351"
+}
+
+flag {
name: "sqlite_apis_35"
is_exported: true
namespace: "system_performance"
diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java
index 9355937..f649e47 100644
--- a/core/java/android/hardware/biometrics/BiometricConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricConstants.java
@@ -164,15 +164,18 @@
int BIOMETRIC_ERROR_POWER_PRESSED = 19;
/**
- * Mandatory biometrics is not in effect.
- * @hide
+ * Identity Check is currently not active.
+ *
+ * This device either doesn't have this feature enabled, or it's not considered in a
+ * high-risk environment that requires extra security measures for accessing sensitive data.
*/
- int BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE = 20;
+ @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
+ int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE = 20;
/**
- * Biometrics is not allowed to verify in apps.
- * @hide
+ * Biometrics is not allowed to verify the user in apps.
*/
+ @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21;
/**
@@ -204,6 +207,8 @@
BIOMETRIC_ERROR_NEGATIVE_BUTTON,
BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL,
BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED,
+ BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE,
+ BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS,
BIOMETRIC_PAUSED_REJECTED})
@Retention(RetentionPolicy.SOURCE)
@interface Errors {}
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index a4f7485f..c690c67 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -87,16 +87,19 @@
BiometricConstants.BIOMETRIC_ERROR_LOCKOUT;
/**
- * Mandatory biometrics is not effective.
- * @hide
+ * Identity Check is currently not active.
+ *
+ * This device either doesn't have this feature enabled, or it's not considered in a
+ * high-risk environment that requires extra security measures for accessing sensitive data.
*/
- public static final int BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE =
- BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
+ @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
+ public static final int BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE =
+ BiometricConstants.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE;
/**
- * Biometrics is not allowed to verify in apps.
- * @hide
+ * Biometrics is not allowed to verify the user in apps.
*/
+ @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS =
BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
@@ -136,7 +139,7 @@
BIOMETRIC_ERROR_NO_HARDWARE,
BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED,
BIOMETRIC_ERROR_LOCKOUT,
- BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE})
+ BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE})
@Retention(RetentionPolicy.SOURCE)
public @interface BiometricError {}
@@ -160,7 +163,7 @@
BIOMETRIC_WEAK,
BIOMETRIC_CONVENIENCE,
DEVICE_CREDENTIAL,
- MANDATORY_BIOMETRICS,
+ IDENTITY_CHECK,
})
@Retention(RetentionPolicy.SOURCE)
@interface Types {}
@@ -239,20 +242,24 @@
int DEVICE_CREDENTIAL = 1 << 15;
/**
- * The bit is used to request for mandatory biometrics.
+ * The bit is used to request for Identity Check.
*
- * <p> The requirements to trigger mandatory biometrics are as follows:
- * 1. User must have enabled the toggle for mandatory biometrics is settings
- * 2. User must have enrollments for all {@link #BIOMETRIC_STRONG} sensors available
- * 3. The device must not be in a trusted location
+ * Identity Check is a feature which requires class 3 biometric authentication to access
+ * sensitive surfaces when the device is outside trusted places.
+ *
+ * <p> The requirements to trigger Identity Check are as follows:
+ * 1. User must have enabled the toggle for Identity Check in settings
+ * 2. User must have enrollments for at least one {@link #BIOMETRIC_STRONG} sensor
+ * 3. The device is determined to be in a high risk environment, for example if it is
+ * outside of the user's trusted locations or fails to meet similar conditions.
+ * 4. The Identity Check requirements bit must be true
* </p>
*
* <p> If all the above conditions are satisfied, only {@link #BIOMETRIC_STRONG} sensors
* will be eligible for authentication, and device credential fallback will be dropped.
- * @hide
*/
- int MANDATORY_BIOMETRICS = 1 << 16;
-
+ @FlaggedApi(Flags.FLAG_IDENTITY_CHECK_API)
+ int IDENTITY_CHECK = 1 << 16;
}
/**
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index df5d864..e23ffeb 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -199,7 +199,7 @@
} else if (mContentView != null && isContentViewMoreOptionsButtonUsed()) {
return true;
} else if (Flags.mandatoryBiometrics()
- && (mAuthenticators & BiometricManager.Authenticators.MANDATORY_BIOMETRICS)
+ && (mAuthenticators & BiometricManager.Authenticators.IDENTITY_CHECK)
!= 0) {
return true;
}
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 26ffa11..52a4898 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -47,3 +47,10 @@
description: "This flag controls Whether to enable fp unlock when screen turns off on udfps devices"
bug: "373792870"
}
+
+flag {
+ name: "identity_check_api"
+ namespace: "biometrics_framework"
+ description: "This flag is for API changes related to Identity Check"
+ bug: "373424727"
+}
diff --git a/core/java/android/os/CombinedVibration.java b/core/java/android/os/CombinedVibration.java
index f1d3957..8fbba4f 100644
--- a/core/java/android/os/CombinedVibration.java
+++ b/core/java/android/os/CombinedVibration.java
@@ -194,7 +194,6 @@
int[] getAvailableVibratorIds();
/** Adapts a {@link VibrationEffect} to a given vibrator. */
- @NonNull
VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect);
}
@@ -442,6 +441,10 @@
boolean hasSameEffects = true;
for (int vibratorId : adapter.getAvailableVibratorIds()) {
VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, mEffect);
+ if (newEffect == null) {
+ // The vibration effect contains unsupported segments and cannot be played.
+ return null;
+ }
combination.addVibrator(vibratorId, newEffect);
hasSameEffects &= mEffect.equals(newEffect);
}
@@ -649,6 +652,10 @@
int vibratorId = mEffects.keyAt(i);
VibrationEffect effect = mEffects.valueAt(i);
VibrationEffect newEffect = adapter.adaptToVibrator(vibratorId, effect);
+ if (newEffect == null) {
+ // The vibration effect contains unsupported segments and cannot be played.
+ return null;
+ }
combination.addVibrator(vibratorId, newEffect);
hasSameEffects &= effect.equals(newEffect);
}
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index 61dd11f..d8094ab 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -37,6 +37,7 @@
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
@@ -1692,6 +1693,147 @@
}
/**
+ * Start building a waveform vibration.
+ *
+ * <p>The waveform envelope builder offers more flexibility for creating waveform effects,
+ * allowing control over vibration amplitude and frequency via smooth transitions between
+ * values.
+ *
+ * <p>Note: To check whether waveform envelope effects are supported, use
+ * {@link Vibrator#areEnvelopeEffectsSupported()}.
+ *
+ * @see VibrationEffect.WaveformEnvelopeBuilder
+ */
+ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ @NonNull
+ public static VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope() {
+ return new WaveformEnvelopeBuilder();
+ }
+
+ /**
+ * A builder for waveform effects described by its envelope.
+ *
+ * <p>Waveform effect envelopes are defined by one or more control points describing a target
+ * vibration amplitude and frequency, and a duration to reach those targets. The vibrator
+ * will perform smooth transitions between control points.
+ *
+ * <p>For example, the following code ramps a vibrator from off to full amplitude at 120Hz over
+ * 100ms, holds that state for 200ms, and then ramps back down over 100ms:
+ *
+ * <pre>{@code
+ * VibrationEffect effect = VibrationEffect.startWaveformEnvelope()
+ * .addControlPoint(1.0f, 120f, 100)
+ * .addControlPoint(1.0f, 120f, 200)
+ * .addControlPoint(0.0f, 120f, 100)
+ * .build();
+ * }</pre>
+ *
+ * <p>It is crucial to ensure that the frequency range used in your effect is compatible with
+ * the device's capabilities. The framework will not play any frequencies that fall partially
+ * or completely outside the device's supported range. It will also not attempt to correct or
+ * modify these frequencies.
+ *
+ * <p>Therefore, it is strongly recommended that you design your haptic effects with the
+ * device's frequency profile in mind. You can obtain the supported frequency range and other
+ * relevant frequency-related information by getting the
+ * {@link android.os.vibrator.VibratorFrequencyProfile} using the
+ * {@link Vibrator#getFrequencyProfile()} method.
+ *
+ * <p>In addition to these limitations, when designing vibration patterns, it is important to
+ * consider the physical limitations of the vibration actuator. These limitations include
+ * factors such as the maximum number of control points allowed in an envelope effect, the
+ * minimum and maximum durations permitted for each control point, and the maximum overall
+ * duration of the effect. If a pattern exceeds the maximum number of allowed control points,
+ * the framework will automatically break down the effect to ensure it plays correctly.
+ *
+ * <p>You can use the following APIs to obtain these limits:
+ * <ul>
+ * <li>Maximum envelope control points: {@link Vibrator#getMaxEnvelopeEffectSize()}</li>
+ * <li>Minimum control point duration:
+ * {@link Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}</li>
+ * <li>Maximum control point duration:
+ * {@link Vibrator#getMaxEnvelopeEffectControlPointDurationMillis()}</li>
+ * <li>Maximum total effect duration: {@link Vibrator#getMaxEnvelopeEffectDurationMillis()}</li>
+ * </ul>
+ *
+ * @see VibrationEffect#startWaveformEnvelope()
+ */
+ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public static final class WaveformEnvelopeBuilder {
+
+ private ArrayList<PwleSegment> mSegments = new ArrayList<>();
+ private float mLastAmplitude = 0f;
+ private float mLastFrequencyHz = 0f;
+
+ private WaveformEnvelopeBuilder() {}
+
+ /**
+ * Adds a new control point to the end of this waveform envelope.
+ *
+ * <p>Amplitude defines the vibrator's strength at this frequency, ranging from 0 (off) to 1
+ * (maximum achievable strength). This value scales linearly with output strength, not
+ * perceived intensity. It's determined by the actuator response curve.
+ *
+ * <p>Frequency must be greater than zero and within the supported range. To determine
+ * the supported range, use {@link Vibrator#getFrequencyProfile()}. This method returns a
+ * {@link android.os.vibrator.VibratorFrequencyProfile} object, which contains the
+ * minimum and maximum frequencies, among other frequency-related information. Creating
+ * effects using frequencies outside this range will result in the vibration not playing.
+ *
+ * <p>Time specifies the duration (in milliseconds) for the vibrator to smoothly transition
+ * from the previous control point to this new one. It must be greater than zero. To
+ * transition as quickly as possible, use
+ * {@link Vibrator#getMinEnvelopeEffectControlPointDurationMillis()}.
+ *
+ * @param amplitude The amplitude value between 0 and 1, inclusive. 0 represents the
+ * vibrator being off, and 1 represents the maximum achievable amplitude
+ * at this frequency.
+ * @param frequencyHz The frequency in Hz, must be greater than zero.
+ * @param timeMillis The transition time in milliseconds.
+ */
+ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ @SuppressWarnings("MissingGetterMatchingBuilder") // No getters to segments once created.
+ @NonNull
+ public WaveformEnvelopeBuilder addControlPoint(
+ @FloatRange(from = 0, to = 1) float amplitude,
+ @FloatRange(from = 0) float frequencyHz, int timeMillis) {
+
+ if (mSegments.isEmpty()) {
+ mLastFrequencyHz = frequencyHz;
+ }
+
+ mSegments.add(new PwleSegment(mLastAmplitude, amplitude, mLastFrequencyHz, frequencyHz,
+ timeMillis));
+
+ mLastAmplitude = amplitude;
+ mLastFrequencyHz = frequencyHz;
+
+ return this;
+ }
+
+ /**
+ * Build the waveform as a single {@link VibrationEffect}.
+ *
+ * <p>The {@link WaveformEnvelopeBuilder} object is still valid after this call, so you can
+ * continue adding more primitives to it and generating more {@link VibrationEffect}s by
+ * calling this method again.
+ *
+ * @return The {@link VibrationEffect} resulting from the list of control points.
+ */
+ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ @NonNull
+ public VibrationEffect build() {
+ if (mSegments.isEmpty()) {
+ throw new IllegalStateException(
+ "WaveformEnvelopeBuilder must have at least one control point to build.");
+ }
+ VibrationEffect effect = new Composed(mSegments, /* repeatIndex= */ -1);
+ effect.validate();
+ return effect;
+ }
+ }
+
+ /**
* A builder for waveform haptic effects.
*
* <p>Waveform vibrations constitute of one or more timed transitions to new sets of vibration
diff --git a/core/java/android/os/vibrator/PwleSegment.java b/core/java/android/os/vibrator/PwleSegment.java
new file mode 100644
index 0000000..9074bde
--- /dev/null
+++ b/core/java/android/os/vibrator/PwleSegment.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.vibrator;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.VibrationEffect;
+import android.os.VibratorInfo;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * A {@link VibrationEffectSegment} that represents a smooth transition from the starting
+ * amplitude and frequency to new values over a specified duration.
+ *
+ * <p>The amplitudes are expressed by float values in the range [0, 1], representing the relative
+ * output acceleration for the vibrator. The frequencies are expressed in hertz by positive finite
+ * float values.
+ * @hide
+ */
+@TestApi
+@FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+public final class PwleSegment extends VibrationEffectSegment {
+ private final float mStartAmplitude;
+ private final float mStartFrequencyHz;
+ private final float mEndAmplitude;
+ private final float mEndFrequencyHz;
+ private final int mDuration;
+
+ PwleSegment(@android.annotation.NonNull Parcel in) {
+ this(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat(), in.readInt());
+ }
+
+ /** @hide */
+ @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public PwleSegment(float startAmplitude, float endAmplitude, float startFrequencyHz,
+ float endFrequencyHz, int duration) {
+ mStartAmplitude = startAmplitude;
+ mEndAmplitude = endAmplitude;
+ mStartFrequencyHz = startFrequencyHz;
+ mEndFrequencyHz = endFrequencyHz;
+ mDuration = duration;
+ }
+
+ public float getStartAmplitude() {
+ return mStartAmplitude;
+ }
+
+ public float getEndAmplitude() {
+ return mEndAmplitude;
+ }
+
+ public float getStartFrequencyHz() {
+ return mStartFrequencyHz;
+ }
+
+ public float getEndFrequencyHz() {
+ return mEndFrequencyHz;
+ }
+
+ @Override
+ public long getDuration() {
+ return mDuration;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof PwleSegment)) {
+ return false;
+ }
+ PwleSegment other = (PwleSegment) o;
+ return Float.compare(mStartAmplitude, other.mStartAmplitude) == 0
+ && Float.compare(mEndAmplitude, other.mEndAmplitude) == 0
+ && Float.compare(mStartFrequencyHz, other.mStartFrequencyHz) == 0
+ && Float.compare(mEndFrequencyHz, other.mEndFrequencyHz) == 0
+ && mDuration == other.mDuration;
+ }
+
+ /** @hide */
+ @Override
+ public boolean areVibrationFeaturesSupported(@NonNull VibratorInfo vibratorInfo) {
+ boolean areFeaturesSupported = vibratorInfo.areEnvelopeEffectsSupported();
+
+ // Check that the frequency is within the supported range
+ float minFrequency = vibratorInfo.getFrequencyProfile().getMinFrequencyHz();
+ float maxFrequency = vibratorInfo.getFrequencyProfile().getMaxFrequencyHz();
+
+ areFeaturesSupported &=
+ mStartFrequencyHz >= minFrequency && mStartFrequencyHz <= maxFrequency
+ && mEndFrequencyHz >= minFrequency && mEndFrequencyHz <= maxFrequency;
+
+ return areFeaturesSupported;
+ }
+
+ /** @hide */
+ @Override
+ public boolean isHapticFeedbackCandidate() {
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ public void validate() {
+ Preconditions.checkArgumentPositive(mStartFrequencyHz,
+ "Start frequency must be greater than zero.");
+ Preconditions.checkArgumentPositive(mEndFrequencyHz,
+ "End frequency must be greater than zero.");
+ Preconditions.checkArgumentPositive(mDuration, "Time must be greater than zero.");
+
+ Preconditions.checkArgumentInRange(mStartAmplitude, 0f, 1f, "startAmplitude");
+ Preconditions.checkArgumentInRange(mEndAmplitude, 0f, 1f, "endAmplitude");
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public PwleSegment resolve(int defaultAmplitude) {
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public PwleSegment scale(float scaleFactor) {
+ float newStartAmplitude = VibrationEffect.scale(mStartAmplitude, scaleFactor);
+ float newEndAmplitude = VibrationEffect.scale(mEndAmplitude, scaleFactor);
+ if (Float.compare(mStartAmplitude, newStartAmplitude) == 0
+ && Float.compare(mEndAmplitude, newEndAmplitude) == 0) {
+ return this;
+ }
+ return new PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz,
+ mEndFrequencyHz,
+ mDuration);
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public PwleSegment scaleLinearly(float scaleFactor) {
+ float newStartAmplitude = VibrationEffect.scaleLinearly(mStartAmplitude, scaleFactor);
+ float newEndAmplitude = VibrationEffect.scaleLinearly(mEndAmplitude, scaleFactor);
+ if (Float.compare(mStartAmplitude, newStartAmplitude) == 0
+ && Float.compare(mEndAmplitude, newEndAmplitude) == 0) {
+ return this;
+ }
+ return new PwleSegment(newStartAmplitude, newEndAmplitude, mStartFrequencyHz,
+ mEndFrequencyHz,
+ mDuration);
+ }
+
+ /** @hide */
+ @NonNull
+ @Override
+ public PwleSegment applyEffectStrength(int effectStrength) {
+ return this;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mStartAmplitude, mEndAmplitude, mStartFrequencyHz, mEndFrequencyHz,
+ mDuration);
+ }
+
+ @Override
+ public String toString() {
+ return "Pwle{startAmplitude=" + mStartAmplitude
+ + ", endAmplitude=" + mEndAmplitude
+ + ", startFrequencyHz=" + mStartFrequencyHz
+ + ", endFrequencyHz=" + mEndFrequencyHz
+ + ", duration=" + mDuration
+ + "}";
+ }
+
+ /** @hide */
+ @Override
+ public String toDebugString() {
+ return String.format(Locale.US, "Pwle=%dms(amplitude=%.2f @ %.2fHz to %.2f @ %.2fHz)",
+ mDuration,
+ mStartAmplitude,
+ mStartFrequencyHz,
+ mEndAmplitude,
+ mEndFrequencyHz);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(PARCEL_TOKEN_PWLE);
+ dest.writeFloat(mStartAmplitude);
+ dest.writeFloat(mEndAmplitude);
+ dest.writeFloat(mStartFrequencyHz);
+ dest.writeFloat(mEndFrequencyHz);
+ dest.writeInt(mDuration);
+ }
+
+ @android.annotation.NonNull
+ public static final Creator<PwleSegment> CREATOR =
+ new Creator<PwleSegment>() {
+ @Override
+ public PwleSegment createFromParcel(Parcel in) {
+ // Skip the type token
+ in.readInt();
+ return new PwleSegment(in);
+ }
+
+ @Override
+ public PwleSegment[] newArray(int size) {
+ return new PwleSegment[size];
+ }
+ };
+}
diff --git a/core/java/android/os/vibrator/VibrationEffectSegment.java b/core/java/android/os/vibrator/VibrationEffectSegment.java
index dadc849..b934e11 100644
--- a/core/java/android/os/vibrator/VibrationEffectSegment.java
+++ b/core/java/android/os/vibrator/VibrationEffectSegment.java
@@ -46,6 +46,7 @@
static final int PARCEL_TOKEN_PRIMITIVE = 2;
static final int PARCEL_TOKEN_STEP = 3;
static final int PARCEL_TOKEN_RAMP = 4;
+ static final int PARCEL_TOKEN_PWLE = 5;
/** Prevent subclassing from outside of this package */
VibrationEffectSegment() {
@@ -223,6 +224,11 @@
return new PrebakedSegment(in);
case PARCEL_TOKEN_PRIMITIVE:
return new PrimitiveSegment(in);
+ case PARCEL_TOKEN_PWLE:
+ if (Flags.normalizedPwleEffects()) {
+ return new PwleSegment(in);
+ }
+ // Fall through if the flag is not enabled.
default:
throw new IllegalStateException(
"Unexpected vibration event type token in parcel.");
diff --git a/core/java/android/service/notification/SystemZenRules.java b/core/java/android/service/notification/SystemZenRules.java
index 1d18643..ebb8569 100644
--- a/core/java/android/service/notification/SystemZenRules.java
+++ b/core/java/android/service/notification/SystemZenRules.java
@@ -19,6 +19,7 @@
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.StringRes;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
@@ -122,7 +123,7 @@
public static String getTriggerDescriptionForScheduleTime(Context context,
@NonNull ScheduleInfo schedule) {
final StringBuilder sb = new StringBuilder();
- String daysSummary = getShortDaysSummary(context, schedule);
+ String daysSummary = getDaysOfWeekShort(context, schedule);
if (daysSummary == null) {
// no use outputting times without dates
return null;
@@ -135,11 +136,35 @@
}
/**
- * Returns an ordered summarized list of the days on which this schedule applies, with
- * adjacent days grouped together ("Sun-Wed" instead of "Sun,Mon,Tue,Wed").
+ * Returns a short, ordered summarized list of the days on which this schedule applies, using
+ * abbreviated week days, with adjacent days grouped together ("Sun-Wed" instead of
+ * "Sun,Mon,Tue,Wed").
*/
@Nullable
- public static String getShortDaysSummary(Context context, @NonNull ScheduleInfo schedule) {
+ public static String getDaysOfWeekShort(Context context, @NonNull ScheduleInfo schedule) {
+ return getDaysSummary(context, R.string.zen_mode_trigger_summary_range_symbol_combination,
+ new SimpleDateFormat("EEE", getLocale(context)), schedule);
+ }
+
+ /**
+ * Returns a string representing the days on which this schedule applies, using full week days,
+ * with adjacent days grouped together (e.g. "Sunday to Wednesday" instead of
+ * "Sunday,Monday,Tuesday,Wednesday").
+ */
+ @Nullable
+ public static String getDaysOfWeekFull(Context context, @NonNull ScheduleInfo schedule) {
+ return getDaysSummary(context, R.string.zen_mode_trigger_summary_range_words,
+ new SimpleDateFormat("EEEE", getLocale(context)), schedule);
+ }
+
+ /**
+ * Returns an ordered summarized list of the days on which this schedule applies, with
+ * adjacent days grouped together. The formatting of each individual day of week is done with
+ * the provided {@link SimpleDateFormat}.
+ */
+ @Nullable
+ private static String getDaysSummary(Context context, @StringRes int rangeFormatResId,
+ SimpleDateFormat dayOfWeekFormat, @NonNull ScheduleInfo schedule) {
// Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or
// "Sun-Mon,Wed,Fri"
final int[] days = schedule.days;
@@ -197,19 +222,18 @@
context.getString(R.string.zen_mode_trigger_summary_divider_text));
}
- SimpleDateFormat dayFormat = new SimpleDateFormat("EEE", getLocale(context));
if (startDay == lastSeenDay) {
// last group was only one day
cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
- sb.append(dayFormat.format(cStart.getTime()));
+ sb.append(dayOfWeekFormat.format(cStart.getTime()));
} else {
// last group was a contiguous group of days, so group them together
cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
cEnd.set(Calendar.DAY_OF_WEEK, daysOfWeek[lastSeenDay]);
sb.append(context.getString(
- R.string.zen_mode_trigger_summary_range_symbol_combination,
- dayFormat.format(cStart.getTime()),
- dayFormat.format(cEnd.getTime())));
+ rangeFormatResId,
+ dayOfWeekFormat.format(cStart.getTime()),
+ dayOfWeekFormat.format(cEnd.getTime())));
}
}
}
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 05dc910..22eec29 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -49,6 +49,7 @@
ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS(
Flags::enableCaptionCompatInsetForceConsumptionAlways, true),
ENABLE_CASCADING_WINDOWS(Flags::enableCascadingWindows, true),
+ ENABLE_TILE_RESIZING(Flags::enableTileResizing, true),
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(
Flags::enableDesktopWindowingWallpaperActivity, true),
ENABLE_DESKTOP_WINDOWING_MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
diff --git a/core/proto/android/service/appwidget.proto b/core/proto/android/service/appwidget.proto
index 97350ef..fb90719 100644
--- a/core/proto/android/service/appwidget.proto
+++ b/core/proto/android/service/appwidget.proto
@@ -20,6 +20,8 @@
option java_multiple_files = true;
option java_outer_classname = "AppWidgetServiceProto";
+import "frameworks/base/core/proto/android/widget/remoteviews.proto";
+
// represents the object holding the dump info of the app widget service
message AppWidgetServiceDumpProto {
repeated WidgetProto widgets = 1; // the array of bound widgets
@@ -38,3 +40,14 @@
optional int32 maxHeight = 9;
optional bool restoreCompleted = 10;
}
+
+// represents a set of widget previews for a particular provider
+message GeneratedPreviewsProto {
+ repeated Preview previews = 1;
+
+ // represents a particular RemoteViews preview, which may be set for multiple categories
+ message Preview {
+ repeated int32 widget_categories = 1;
+ optional android.widget.RemoteViewsProto views = 2;
+ }
+}
\ No newline at end of file
diff --git a/core/res/res/values-watch-v36/colors.xml b/core/res/res/values-watch-v36/colors.xml
index 6cb9b85..4bc2a66 100644
--- a/core/res/res/values-watch-v36/colors.xml
+++ b/core/res/res/values-watch-v36/colors.xml
@@ -15,9 +15,4 @@
-->
<!-- TODO(b/372524566): update color token's value to match material3 design. -->
<resources>
- <color name="system_primary_dark">#E9DDFF</color>
- <color name="system_primary_fixed_dim">#D0BCFF</color>
- <color name="system_on_primary_dark">#210F48</color>
- <color name="system_primary_container_dark">#4D3D76</color>
- <color name="system_on_primary_container_dark">#F6EDFF</color>
-</resources>
+</resources>
\ No newline at end of file
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 7aca535..732c316 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5348,6 +5348,8 @@
<string name="zen_mode_trigger_summary_divider_text">,\u0020</string>
<!-- [CHAR LIMIT=40] General template for a symbolic start - end range in a text summary, used for the trigger description of a Zen mode -->
<string name="zen_mode_trigger_summary_range_symbol_combination"><xliff:g id="start" example="Sun">%1$s</xliff:g> - <xliff:g id="end" example="Thu">%2$s</xliff:g></string>
+ <!-- [CHAR LIMIT=40] General template for a start - end range in a text summary, used for the trigger description of a Zen mode -->
+ <string name="zen_mode_trigger_summary_range_words"><xliff:g id="start" example="Sunday">%1$s</xliff:g> to <xliff:g id="end" example="Thursday">%2$s</xliff:g></string>
<!-- [CHAR LIMIT=40] Event-based rule calendar option value for any calendar, used for the trigger description of a Zen mode -->
<string name="zen_mode_trigger_event_calendar_any">Any calendar</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ecc3afe..4dbeb2f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2657,6 +2657,7 @@
<java-symbol type="string" name="zen_mode_implicit_deactivated" />
<java-symbol type="string" name="zen_mode_trigger_summary_divider_text" />
<java-symbol type="string" name="zen_mode_trigger_summary_range_symbol_combination" />
+ <java-symbol type="string" name="zen_mode_trigger_summary_range_words" />
<java-symbol type="string" name="zen_mode_trigger_event_calendar_any" />
<java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAlertUnitTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAlertUnitTest.java
index 7afdde2..9cfb9af 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAlertUnitTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioAlertUnitTest.java
@@ -21,6 +21,7 @@
import android.os.Parcel;
import android.platform.test.annotations.EnableFlags;
+import com.google.common.primitives.Ints;
import com.google.common.truth.Expect;
import org.junit.Rule;
@@ -34,6 +35,20 @@
private static final int TEST_FLAGS = 0;
private static final int CREATOR_ARRAY_SIZE = 3;
+ private static final int TEST_STATUS = RadioAlert.STATUS_ACTUAL;
+ private static final int TEST_TYPE = RadioAlert.MESSAGE_TYPE_ALERT;
+ private static final int[] TEST_CATEGORIES_1 = new int[]{RadioAlert.CATEGORY_CBRNE,
+ RadioAlert.CATEGORY_GEO};
+ private static final int[] TEST_CATEGORIES_2 = new int[]{RadioAlert.CATEGORY_CBRNE,
+ RadioAlert.CATEGORY_FIRE};
+ private static final int TEST_URGENCY_1 = RadioAlert.URGENCY_EXPECTED;
+ private static final int TEST_URGENCY_2 = RadioAlert.URGENCY_FUTURE;
+ private static final int TEST_SEVERITY_1 = RadioAlert.SEVERITY_SEVERE;
+ private static final int TEST_SEVERITY_2 = RadioAlert.SEVERITY_MODERATE;
+ private static final int TEST_CERTAINTY_1 = RadioAlert.CERTAINTY_POSSIBLE;
+ private static final int TEST_CERTAINTY_2 = RadioAlert.CERTAINTY_UNLIKELY;
+ private static final String TEST_DESCRIPTION_MESSAGE_1 = "Test Alert Description Message 1.";
+ private static final String TEST_DESCRIPTION_MESSAGE_2 = "Test Alert Description Message 2.";
private static final String TEST_GEOCODE_VALUE_NAME = "SAME";
private static final String TEST_GEOCODE_VALUE_1 = "006109";
private static final String TEST_GEOCODE_VALUE_2 = "006009";
@@ -54,6 +69,18 @@
List.of(TEST_POLYGON), List.of(TEST_GEOCODE_1));
private static final RadioAlert.AlertArea TEST_AREA_2 = new RadioAlert.AlertArea(
new ArrayList<>(), List.of(TEST_GEOCODE_1, TEST_GEOCODE_2));
+ private static final List<RadioAlert.AlertArea> TEST_AREA_LIST_1 = List.of(TEST_AREA_1);
+ private static final List<RadioAlert.AlertArea> TEST_AREA_LIST_2 = List.of(TEST_AREA_2);
+ private static final String TEST_LANGUAGE_1 = "en-US";
+
+ private static final RadioAlert.AlertInfo TEST_ALERT_INFO_1 = new RadioAlert.AlertInfo(
+ TEST_CATEGORIES_1, TEST_URGENCY_1, TEST_SEVERITY_1, TEST_CERTAINTY_1,
+ TEST_DESCRIPTION_MESSAGE_1, TEST_AREA_LIST_1, TEST_LANGUAGE_1);
+ private static final RadioAlert.AlertInfo TEST_ALERT_INFO_2 = new RadioAlert.AlertInfo(
+ TEST_CATEGORIES_2, TEST_URGENCY_2, TEST_SEVERITY_2, TEST_CERTAINTY_2,
+ TEST_DESCRIPTION_MESSAGE_2, TEST_AREA_LIST_2, /* language= */ null);
+ private static final RadioAlert TEST_ALERT = new RadioAlert(TEST_STATUS, TEST_TYPE,
+ List.of(TEST_ALERT_INFO_1, TEST_ALERT_INFO_2));
@Rule
public final Expect mExpect = Expect.create();
@@ -374,4 +401,209 @@
mExpect.withMessage("Non-alert-area object").that(TEST_AREA_1)
.isNotEqualTo(TEST_GEOCODE_1);
}
+
+ @Test
+ public void constructor_withNullCategories_forAlertInfo_fails() {
+ NullPointerException thrown = assertThrows(NullPointerException.class, () ->
+ new RadioAlert.AlertInfo(/* categories= */ null, TEST_URGENCY_1, TEST_SEVERITY_1,
+ TEST_CERTAINTY_1, TEST_DESCRIPTION_MESSAGE_1, TEST_AREA_LIST_1,
+ TEST_LANGUAGE_1));
+
+ mExpect.withMessage("Exception for alert info constructor with null categories")
+ .that(thrown).hasMessageThat().contains("Categories can not be null");
+ }
+
+ @Test
+ public void constructor_withNullAreaList_forAlertInfo_fails() {
+ NullPointerException thrown = assertThrows(NullPointerException.class, () ->
+ new RadioAlert.AlertInfo(TEST_CATEGORIES_1, TEST_URGENCY_1, TEST_SEVERITY_1,
+ TEST_CERTAINTY_1, TEST_DESCRIPTION_MESSAGE_1, /* areaList= */ null,
+ TEST_LANGUAGE_1));
+
+ mExpect.withMessage("Exception for alert info constructor with null area list")
+ .that(thrown).hasMessageThat().contains("Area list can not be null");
+ }
+
+ @Test
+ public void getCategories_forAlertInfo() {
+ mExpect.withMessage("Radio alert info categories")
+ .that(Ints.asList(TEST_ALERT_INFO_1.getCategories()))
+ .containsExactlyElementsIn(Ints.asList(TEST_CATEGORIES_1));
+ }
+
+ @Test
+ public void getUrgency_forAlertInfo() {
+ mExpect.withMessage("Radio alert info urgency")
+ .that(TEST_ALERT_INFO_1.getUrgency()).isEqualTo(TEST_URGENCY_1);
+ }
+
+ @Test
+ public void getSeverity_forAlertInfo() {
+ mExpect.withMessage("Radio alert info severity")
+ .that(TEST_ALERT_INFO_1.getSeverity()).isEqualTo(TEST_SEVERITY_1);
+ }
+
+ @Test
+ public void getCertainty_forAlertInfo() {
+ mExpect.withMessage("Radio alert info certainty")
+ .that(TEST_ALERT_INFO_1.getCertainty()).isEqualTo(TEST_CERTAINTY_1);
+ }
+
+ @Test
+ public void getDescription_forAlertInfo() {
+ mExpect.withMessage("Radio alert info description")
+ .that(TEST_ALERT_INFO_1.getDescription()).isEqualTo(TEST_DESCRIPTION_MESSAGE_1);
+ }
+
+ @Test
+ public void getAreas_forAlertInfo() {
+ mExpect.withMessage("Radio alert info areas")
+ .that(TEST_ALERT_INFO_1.getAreas()).containsAtLeastElementsIn(TEST_AREA_LIST_1);
+ }
+
+ @Test
+ public void getLanguage_forAlertInfo() {
+ mExpect.withMessage("Radio alert language")
+ .that(TEST_ALERT_INFO_1.getLanguage()).isEqualTo(TEST_LANGUAGE_1);
+ }
+
+ @Test
+ public void describeContents_forAlertInfo() {
+ mExpect.withMessage("Contents of alert info")
+ .that(TEST_ALERT_INFO_1.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void writeToParcel_forAlertInfoWithNullLanguage() {
+ Parcel parcel = Parcel.obtain();
+
+ TEST_ALERT_INFO_2.writeToParcel(parcel, TEST_FLAGS);
+
+ parcel.setDataPosition(0);
+ RadioAlert.AlertInfo alertInfoFromParcel = RadioAlert.AlertInfo.CREATOR
+ .createFromParcel(parcel);
+ mExpect.withMessage("Alert info from parcel with null language")
+ .that(alertInfoFromParcel).isEqualTo(TEST_ALERT_INFO_2);
+ }
+
+ @Test
+ public void writeToParcel_forAlertInfoWithNonnullLanguage() {
+ Parcel parcel = Parcel.obtain();
+
+ TEST_ALERT_INFO_1.writeToParcel(parcel, TEST_FLAGS);
+
+ parcel.setDataPosition(0);
+ RadioAlert.AlertInfo alertInfoFromParcel = RadioAlert.AlertInfo.CREATOR
+ .createFromParcel(parcel);
+ mExpect.withMessage("Alert info with nonnull language from parcel")
+ .that(alertInfoFromParcel).isEqualTo(TEST_ALERT_INFO_1);
+ }
+
+ @Test
+ public void newArray_forAlertInfoCreator() {
+ RadioAlert.AlertInfo[] alertInfos = RadioAlert.AlertInfo.CREATOR
+ .newArray(CREATOR_ARRAY_SIZE);
+
+ mExpect.withMessage("Alert infos").that(alertInfos).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
+ public void hashCode_withSameAlertInfos() {
+ RadioAlert.AlertInfo alertInfoCompared = new RadioAlert.AlertInfo(
+ TEST_CATEGORIES_1, TEST_URGENCY_1, TEST_SEVERITY_1, TEST_CERTAINTY_1,
+ TEST_DESCRIPTION_MESSAGE_1, TEST_AREA_LIST_1, TEST_LANGUAGE_1);
+
+ mExpect.withMessage("Hash code of the same alert info")
+ .that(alertInfoCompared.hashCode()).isEqualTo(TEST_ALERT_INFO_1.hashCode());
+ }
+
+ @Test
+ public void constructor_forRadioAlert() {
+ NullPointerException thrown = assertThrows(NullPointerException.class, () ->
+ new RadioAlert(TEST_STATUS, TEST_TYPE, /* infoList= */ null));
+
+ mExpect.withMessage("Exception for alert constructor with null alert info list")
+ .that(thrown).hasMessageThat().contains("Alert info list can not be null");
+ }
+
+ @Test
+ public void equals_withDifferentAlertInfo() {
+ mExpect.withMessage("Different alert info").that(TEST_ALERT_INFO_1)
+ .isNotEqualTo(TEST_ALERT_INFO_2);
+ }
+
+ @Test
+ @SuppressWarnings("TruthIncompatibleType")
+ public void equals_withDifferentTypeObject_forAlertInfo() {
+ mExpect.withMessage("Non-alert-info object").that(TEST_ALERT_INFO_1)
+ .isNotEqualTo(TEST_AREA_1);
+ }
+
+ @Test
+ public void getStatus() {
+ mExpect.withMessage("Radio alert status").that(TEST_ALERT.getStatus())
+ .isEqualTo(TEST_STATUS);
+ }
+
+ @Test
+ public void getMessageType() {
+ mExpect.withMessage("Radio alert message type")
+ .that(TEST_ALERT.getMessageType()).isEqualTo(TEST_TYPE);
+ }
+
+ @Test
+ public void getInfoList() {
+ mExpect.withMessage("Radio alert info list").that(TEST_ALERT.getInfoList())
+ .containsExactly(TEST_ALERT_INFO_1, TEST_ALERT_INFO_2);
+ }
+
+ @Test
+ public void describeContents() {
+ mExpect.withMessage("Contents of radio alert")
+ .that(TEST_ALERT.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void writeToParcel() {
+ Parcel parcel = Parcel.obtain();
+
+ TEST_ALERT.writeToParcel(parcel, TEST_FLAGS);
+
+ parcel.setDataPosition(0);
+ RadioAlert alertFromParcel = RadioAlert.CREATOR.createFromParcel(parcel);
+ mExpect.withMessage("Alert from parcel").that(alertFromParcel)
+ .isEqualTo(TEST_ALERT);
+ }
+
+ @Test
+ public void hashCode_withSameAlerts() {
+ RadioAlert alertCompared = new RadioAlert(TEST_STATUS, TEST_TYPE,
+ List.of(TEST_ALERT_INFO_1, TEST_ALERT_INFO_2));
+
+ mExpect.withMessage("Hash code of the same alert")
+ .that(alertCompared.hashCode()).isEqualTo(TEST_ALERT.hashCode());
+ }
+
+ @Test
+ public void newArray_forAlertCreator() {
+ RadioAlert[] alerts = RadioAlert.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ mExpect.withMessage("Alerts").that(alerts).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
+ public void equals_withDifferentAlert() {
+ RadioAlert differentAlert = new RadioAlert(TEST_STATUS, TEST_TYPE,
+ List.of(TEST_ALERT_INFO_2));
+
+ mExpect.withMessage("Different alert").that(TEST_ALERT)
+ .isNotEqualTo(differentAlert);
+ }
+
+ @Test
+ @SuppressWarnings("TruthIncompatibleType")
+ public void equals_withDifferentTypeObject() {
+ mExpect.withMessage("Non-alert object").that(TEST_ALERT)
+ .isNotEqualTo(TEST_ALERT_INFO_2);
+ }
}
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
index 013117e..1721e1e 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
@@ -34,6 +34,7 @@
import android.os.Parcel;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
@@ -87,6 +88,8 @@
TEST_EDITOR_INFO.targetInputMethodUser = UserHandle.of(TEST_USER_ID);
}
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
/**
* Makes sure that {@code null} {@link EditorInfo#targetInputMethodUser} can be copied via
* {@link Parcel}.
@@ -522,7 +525,9 @@
info.setSupportedHandwritingGestures(Arrays.asList(SelectGesture.class));
info.setSupportedHandwritingGesturePreviews(
Stream.of(SelectGesture.class).collect(Collectors.toSet()));
- if (Flags.editorinfoHandwritingEnabled()) {
+ final boolean isStylusHandwritingEnabled =
+ mFlagsValueProvider.getBoolean(Flags.FLAG_EDITORINFO_HANDWRITING_ENABLED);
+ if (isStylusHandwritingEnabled) {
info.setStylusHandwritingEnabled(true);
}
info.packageName = "android.view.inputmethod";
@@ -548,8 +553,7 @@
+ "prefix2: hintLocales=[en,es,zh]\n"
+ "prefix2: supportedHandwritingGestureTypes=SELECT\n"
+ "prefix2: supportedHandwritingGesturePreviewTypes=SELECT\n"
- + "prefix2: isStylusHandwritingEnabled="
- + Flags.editorinfoHandwritingEnabled() + "\n"
+ + "prefix2: isStylusHandwritingEnabled=" + isStylusHandwritingEnabled + "\n"
+ "prefix2: contentMimeTypes=[image/png]\n"
+ "prefix2: targetInputMethodUserId=10\n");
}
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
index 61bf137..44b2d90 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
@@ -26,6 +26,8 @@
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.Parcel;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,8 +45,9 @@
public class InputMethodInfoTest {
@Rule
- public SetFlagsRule mSetFlagsRule =
- new SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+ public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
@Test
public void testEqualsAndHashCode() throws Exception {
@@ -70,7 +73,7 @@
assertThat(imi.supportsInlineSuggestionsWithTouchExploration(), is(false));
assertThat(imi.supportsStylusHandwriting(), is(false));
assertThat(imi.createStylusHandwritingSettingsActivityIntent(), equalTo(null));
- if (Flags.imeSwitcherRevampApi()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_IME_SWITCHER_REVAMP_API)) {
assertThat(imi.createImeLanguageSettingsActivityIntent(), equalTo(null));
}
}
@@ -121,9 +124,8 @@
}
@Test
+ @EnableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_IME)
public void testIsVirtualDeviceOnly() throws Exception {
- mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_IME);
-
final InputMethodInfo imi = buildInputMethodForTest(R.xml.ime_meta_virtual_device_only);
assertThat(imi.isVirtualDeviceOnly(), is(true));
diff --git a/core/tests/overlaytests/handle_config_change/Android.bp b/core/tests/overlaytests/handle_config_change/Android.bp
index 2b31d0a..42b60e8 100644
--- a/core/tests/overlaytests/handle_config_change/Android.bp
+++ b/core/tests/overlaytests/handle_config_change/Android.bp
@@ -38,7 +38,7 @@
"device-tests",
],
// All APKs required by the tests
- data: [
+ device_common_data: [
":OverlayResApp",
],
per_testcase_directory: true,
diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
index 8acf2ed..8a250bd 100644
--- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java
+++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java
@@ -46,6 +46,7 @@
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.RequiresFlagsEnabled;
import com.android.internal.R;
@@ -401,6 +402,21 @@
}
@Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void computeLegacyPattern_effectsViaStartWaveformEnvelope() {
+ // Effects created via startWaveformEnvelope are not expected to be converted to long[]
+ // patterns, as they are not configured to always play with the default amplitude.
+ VibrationEffect effect = VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
+ .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
+ .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
+ .build();
+
+ assertNull(effect.computeCreateWaveformOffOnTimingsOrNull());
+ }
+
+ @Test
public void computeLegacyPattern_effectsViaStartWaveform() {
// Effects created via startWaveform are not expected to be converted to long[] patterns, as
// they are not configured to always play with the default amplitude.
@@ -595,6 +611,45 @@
}
@Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testValidateWaveformEnvelopeBuilder() {
+ VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
+ .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50)
+ .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80)
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40)
+ .build()
+ .validate();
+
+ assertThrows(IllegalStateException.class,
+ () -> VibrationEffect.startWaveformEnvelope().build().validate());
+ assertThrows(IllegalArgumentException.class,
+ () -> VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ -1.0f, /*frequencyHz=*/ 60f,
+ /*timeMillis=*/ 20)
+ .build()
+ .validate());
+ assertThrows(IllegalArgumentException.class,
+ () -> VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ 1.1f, /*frequencyHz=*/ 60f,
+ /*timeMillis=*/ 20)
+ .build()
+ .validate());
+ assertThrows(IllegalArgumentException.class,
+ () -> VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 0f,
+ /*timeMillis=*/ 20)
+ .build()
+ .validate());
+ assertThrows(IllegalArgumentException.class,
+ () -> VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 100f,
+ /*timeMillis=*/ 0)
+ .build()
+ .validate());
+ }
+
+ @Test
public void testValidateWaveformBuilder() {
// Cover builder methods
VibrationEffect.startWaveform(targetAmplitude(1))
@@ -1190,6 +1245,13 @@
.addTransition(Duration.ofMillis(500), targetAmplitude(0))
.build()
.isHapticFeedbackCandidate());
+ assertFalse(VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 200)
+ .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
+ .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 800)
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
+ .build()
+ .isHapticFeedbackCandidate());
}
@Test
@@ -1203,6 +1265,12 @@
.addTransition(Duration.ofMillis(300), targetAmplitude(0))
.build()
.isHapticFeedbackCandidate());
+ assertTrue(VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500)
+ .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400)
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 100)
+ .build()
+ .isHapticFeedbackCandidate());
}
@Test
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
index b6db6d9..61c09f2 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
@@ -29,6 +29,8 @@
static_libs: [
"WindowManager-Shell",
"platform-screenshot-diff-core",
+ "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot
+ "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib)
],
asset_dirs: ["goldens/robolectric"],
manifest: "AndroidManifestRobolectric.xml",
@@ -63,6 +65,8 @@
],
static_libs: [
"WindowManager-Shell",
+ "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot
+ "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib)
"junit",
"androidx.test.runner",
"androidx.test.rules",
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
index b4bdaea..72d0d5e4 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
@@ -18,6 +18,7 @@
<application android:debuggable="true" android:supportsRtl="true">
<activity
android:name="platform.test.screenshot.ScreenshotActivity"
+ android:theme="@style/Theme.PlatformUi.Screenshot"
android:exported="true">
</activity>
</application>
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
index 736bca7..5b429c0 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
index 736bca7..6028fa2 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
index e540b45..a163d92 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png
index e540b45..25d2e34c 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
index f09969d..8cf3ce9 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
@@ -15,10 +15,12 @@
*/
package com.android.wm.shell.bubbles
+import android.graphics.Color
import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.android.wm.shell.R
import com.android.wm.shell.shared.bubbles.BubblePopupView
import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager
-import com.android.wm.shell.R
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,6 +50,10 @@
fun bubblesEducation() {
screenshotRule.screenshotTest("bubbles_education") { activity ->
activity.actionBar?.hide()
+ // Set the background color of the activity to be something not from the theme to
+ // ensure good contrast between the education view and the background
+ val rootView = activity.window.decorView.findViewById(android.R.id.content) as ViewGroup
+ rootView.setBackgroundColor(Color.RED)
val view =
LayoutInflater.from(activity)
.inflate(R.layout.bubble_bar_stack_education, null) as BubblePopupView
diff --git a/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml b/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml
new file mode 100644
index 0000000..ce2e8be
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml
@@ -0,0 +1,37 @@
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<com.android.wm.shell.windowdecor.tiling.TilingDividerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:id="@+id/divider_bar">
+
+ <com.android.wm.shell.common.split.DividerHandleView
+ android:id="@+id/docked_divider_handle"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:layout_gravity="center"
+ android:contentDescription="@string/accessibility_divider"
+ android:background="@null"/>
+
+ <com.android.wm.shell.common.split.DividerRoundedCorner
+ android:id="@+id/docked_divider_rounded_corner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+
+</com.android.wm.shell.windowdecor.tiling.TilingDividerView>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
index 6c83d88..eb7ef14 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
@@ -63,8 +63,19 @@
* @param currentBounds {@link Rect} of the current animation bounds.
* @param fraction progress of the animation ranged from 0f to 1f.
*/
- public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
- Rect currentBounds, float fraction);
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {}
+
+ /**
+ * Animates the internal {@link #mLeash} by a given fraction for a config-at-end transition.
+ * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
+ * call apply on this transaction, it should be applied on the caller side.
+ * @param scale scaling to apply onto the overlay.
+ * @param fraction progress of the animation ranged from 0f to 1f.
+ * @param endBounds the final bounds PiP is animating into.
+ */
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ float scale, float fraction, Rect endBounds) {}
/** A {@link PipContentOverlay} uses solid color. */
public static final class PipColorOverlay extends PipContentOverlay {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
index bdbd4cf..6c04e2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
@@ -103,7 +103,8 @@
mHoveringHeight = mHeight > mWidth ? ((int) (mHeight * 1.5f)) : mHeight;
}
- void setIsLeftRightSplit(boolean isLeftRightSplit) {
+ /** sets whether it's a left/right or top/bottom split */
+ public void setIsLeftRightSplit(boolean isLeftRightSplit) {
mIsLeftRightSplit = isLeftRightSplit;
updateDimens();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
index 834c15d..d5aaf75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
@@ -98,7 +98,11 @@
return false;
}
- void setIsLeftRightSplit(boolean isLeftRightSplit) {
+ /**
+ * Set whether the rounded corner is for a left/right split.
+ * @param isLeftRightSplit whether it's a left/right split or top/bottom split.
+ */
+ public void setIsLeftRightSplit(boolean isLeftRightSplit) {
mIsLeftRightSplit = isLeftRightSplit;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 7f54786..0942e05 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -130,6 +130,7 @@
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController;
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel;
import dagger.Binds;
import dagger.Lazy;
@@ -649,7 +650,8 @@
InteractionJankMonitor interactionJankMonitor,
InputManager inputManager,
FocusTransitionObserver focusTransitionObserver,
- DesktopModeEventLogger desktopModeEventLogger) {
+ DesktopModeEventLogger desktopModeEventLogger,
+ DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
dragAndDropController, transitions, keyguardManager,
@@ -661,8 +663,32 @@
desktopModeLoggerTransitionObserver, launchAdjacentController,
recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
recentTasksController.orElse(null), interactionJankMonitor, mainHandler,
- inputManager, focusTransitionObserver,
- desktopModeEventLogger);
+ inputManager, focusTransitionObserver, desktopModeEventLogger,
+ desktopTilingDecorViewModel);
+ }
+
+ @WMSingleton
+ @Provides
+ static DesktopTilingDecorViewModel provideDesktopTilingViewModel(Context context,
+ DisplayController displayController,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ SyncTransactionQueue syncQueue,
+ Transitions transitions,
+ ShellTaskOrganizer shellTaskOrganizer,
+ ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
+ ReturnToDragStartAnimator returnToDragStartAnimator,
+ @DynamicOverride DesktopRepository desktopRepository) {
+ return new DesktopTilingDecorViewModel(
+ context,
+ displayController,
+ rootTaskDisplayAreaOrganizer,
+ syncQueue,
+ transitions,
+ shellTaskOrganizer,
+ toggleResizeDesktopTaskTransitionHandler,
+ returnToDragStartAnimator,
+ desktopRepository
+ );
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index bc78e43..eec2ba5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -118,6 +118,7 @@
import com.android.wm.shell.transition.OneShotRemoteHandler
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
@@ -125,6 +126,7 @@
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.requestingImmersive
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
import java.io.PrintWriter
import java.util.Optional
import java.util.concurrent.Executor
@@ -163,6 +165,7 @@
private val inputManager: InputManager,
private val focusTransitionObserver: FocusTransitionObserver,
private val desktopModeEventLogger: DesktopModeEventLogger,
+ private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel,
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
@@ -237,6 +240,7 @@
override fun onAnimationStateChanged(running: Boolean) {
logV("Recents animation state changed running=%b", running)
recentsAnimationRunning = running
+ desktopTilingDecorViewModel.onOverviewAnimationStateChange(running)
}
}
)
@@ -492,6 +496,7 @@
taskInfo: RunningTaskInfo,
): ((IBinder) -> Unit)? {
val taskId = taskInfo.taskId
+ desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
removeWallpaperActivity(wct)
}
@@ -532,6 +537,7 @@
/** Move a task with given `taskId` to fullscreen */
fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) {
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
+ desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, taskId)
moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource)
}
}
@@ -539,6 +545,7 @@
/** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */
fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) {
getFocusedFreeformTask(displayId)?.let {
+ desktopTilingDecorViewModel.removeTaskIfTiled(displayId, it.taskId)
moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource)
}
}
@@ -546,6 +553,7 @@
/** Move a desktop app to split screen. */
fun moveToSplit(task: RunningTaskInfo) {
logV( "moveToSplit taskId=%s", task.taskId)
+ desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
val wct = WindowContainerTransaction()
wct.setBounds(task.token, Rect())
// Rather than set windowing mode to multi-window at task level, set it to
@@ -643,6 +651,11 @@
@JvmOverloads
fun moveTaskToFront(taskInfo: RunningTaskInfo, remoteTransition: RemoteTransition? = null) {
logV("moveTaskToFront taskId=%s", taskInfo.taskId)
+ // If a task is tiled, another task should be brought to foreground with it so let
+ // tiling controller handle the request.
+ if (desktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo)) {
+ return
+ }
val wct = WindowContainerTransaction()
wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
@@ -804,13 +817,13 @@
} else {
// Save current bounds so that task can be restored back to original bounds if necessary
// and toggle to the stable bounds.
+ desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
}
-
val shouldRestoreToSnap =
isMaximized && isTaskSnappedToHalfScreen(taskInfo, destinationBounds)
@@ -918,7 +931,20 @@
position: SnapPosition,
resizeTrigger: ResizeTrigger,
motionEvent: MotionEvent?,
+ desktopWindowDecoration: DesktopModeWindowDecoration,
) {
+ if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) {
+ val isTiled = desktopTilingDecorViewModel.snapToHalfScreen(
+ taskInfo,
+ desktopWindowDecoration,
+ position,
+ currentDragBounds,
+ )
+ if (isTiled) {
+ taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true)
+ }
+ return
+ }
val destinationBounds = getSnapBounds(taskInfo, position)
desktopModeEventLogger.logTaskResizingEnded(
resizeTrigger,
@@ -938,7 +964,7 @@
taskSurface,
startBounds = currentDragBounds,
endBounds = destinationBounds,
- isResizable = taskInfo.isResizeable
+ isResizable = taskInfo.isResizeable,
)
}
return
@@ -958,6 +984,7 @@
currentDragBounds: Rect,
dragStartBounds: Rect,
motionEvent: MotionEvent,
+ desktopModeWindowDecoration: DesktopModeWindowDecoration,
) {
releaseVisualIndicator()
if (!taskInfo.isResizeable && DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) {
@@ -992,6 +1019,7 @@
position,
resizeTrigger,
motionEvent,
+ desktopModeWindowDecoration,
)
}
}
@@ -1499,7 +1527,11 @@
addPendingMinimizeTransition(transition, taskIdToMinimize)
return wct
}
- return if (wct.isEmpty) null else wct
+ if (!wct.isEmpty) {
+ desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+ return wct
+ }
+ return null
}
private fun handleFullscreenTaskLaunch(
@@ -1565,6 +1597,7 @@
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
taskRepository.addClosingTask(task.displayId, task.taskId)
+ desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
}
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
@@ -1812,6 +1845,7 @@
taskBounds: Rect
) {
if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return
+ desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
updateVisualIndicator(taskInfo, taskSurface, inputX, taskBounds.top.toFloat(),
DragStartState.FROM_FREEFORM)
}
@@ -1861,6 +1895,7 @@
validDragArea: Rect,
dragStartBounds: Rect,
motionEvent: MotionEvent,
+ desktopModeWindowDecoration: DesktopModeWindowDecoration,
) {
if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
return
@@ -1887,6 +1922,7 @@
currentDragBounds,
dragStartBounds,
motionEvent,
+ desktopModeWindowDecoration,
)
}
IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
@@ -1897,6 +1933,7 @@
currentDragBounds,
dragStartBounds,
motionEvent,
+ desktopModeWindowDecoration,
)
}
IndicatorType.NO_INDICATOR -> {
@@ -2090,6 +2127,7 @@
// TODO(b/366397912): Support full multi-user mode in Windowing.
override fun onUserChanged(newUserId: Int, userContext: Context) {
userId = newUserId
+ desktopTilingDecorViewModel.onUserChange()
}
/** Called when a task's info changes. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
index 5381a62..740b9af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
@@ -23,6 +23,7 @@
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
+import android.content.pm.ActivityInfo;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -33,10 +34,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+import com.android.wm.shell.pip2.phone.PipAppIconOverlay;
import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.pip.PipContentOverlay;
/**
* Animator that handles bounds animations for entering PIP.
@@ -59,6 +63,10 @@
private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
+ Matrix mTransformTensor = new Matrix();
+ final float[] mMatrixTmp = new float[9];
+ @Nullable private PipContentOverlay mContentOverlay;
+
// Internal state representing initial transform - cached to avoid recalculation.
private final PointF mInitScale = new PointF();
@@ -67,9 +75,6 @@
private final PointF mInitActivityScale = new PointF();
private final PointF mInitActivityPos = new PointF();
- Matrix mTransformTensor = new Matrix();
- final float[] mMatrixTmp = new float[9];
-
public PipEnterAnimator(Context context,
@NonNull SurfaceControl leash,
SurfaceControl.Transaction startTransaction,
@@ -161,10 +166,15 @@
mRectEvaluator.evaluate(fraction, initCrop, endCrop);
tx.setCrop(mLeash, mAnimatedRect);
+ mTransformTensor.reset();
mTransformTensor.setScale(scaleX, scaleY);
mTransformTensor.postTranslate(posX, posY);
mTransformTensor.postRotate(degrees);
tx.setMatrix(mLeash, mTransformTensor, mMatrixTmp);
+
+ if (mContentOverlay != null) {
+ mContentOverlay.onAnimationUpdate(tx, 1f / scaleX, fraction, mEndBounds);
+ }
}
// no-ops
@@ -200,4 +210,48 @@
}
PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop);
}
+
+ /**
+ * Initializes and attaches an app icon overlay on top of the PiP layer.
+ */
+ public void setAppIconContentOverlay(Context context, Rect appBounds, Rect destinationBounds,
+ ActivityInfo activityInfo, int appIconSizePx) {
+ reattachAppIconOverlay(
+ new PipAppIconOverlay(context, appBounds, destinationBounds,
+ new IconProvider(context).getIcon(activityInfo), appIconSizePx));
+ }
+
+ private void reattachAppIconOverlay(PipAppIconOverlay overlay) {
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ if (mContentOverlay != null) {
+ mContentOverlay.detach(tx);
+ }
+ mContentOverlay = overlay;
+ mContentOverlay.attach(tx, mLeash);
+ }
+
+ /**
+ * Clears the {@link #mContentOverlay}, this should be done after the content overlay is
+ * faded out.
+ */
+ public void clearAppIconOverlay() {
+ if (mContentOverlay == null) {
+ return;
+ }
+ SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+ mContentOverlay.detach(tx);
+ mContentOverlay = null;
+ }
+
+ /**
+ * @return the app icon overlay leash; null if no overlay is attached.
+ */
+ @Nullable
+ public SurfaceControl getContentOverlayLeash() {
+ if (mContentOverlay == null) {
+ return null;
+ }
+ return mContentOverlay.getLeash();
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
new file mode 100644
index 0000000..b4cf890
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.shared.pip.PipContentOverlay;
+
+/** A {@link PipContentOverlay} shows app icon on solid color background. */
+public final class PipAppIconOverlay extends PipContentOverlay {
+ private static final String TAG = PipAppIconOverlay.class.getSimpleName();
+ // The maximum size for app icon in pixel.
+ private static final int MAX_APP_ICON_SIZE_DP = 72;
+
+ private final Context mContext;
+ private final int mAppIconSizePx;
+ private final Rect mAppBounds;
+ private final int mOverlayHalfSize;
+ private final Matrix mTmpTransform = new Matrix();
+ private final float[] mTmpFloat9 = new float[9];
+
+ private Bitmap mBitmap;
+
+ public PipAppIconOverlay(Context context, Rect appBounds, Rect destinationBounds,
+ Drawable appIcon, int appIconSizePx) {
+ mContext = context;
+ final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
+ MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics());
+ mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx);
+
+ final int overlaySize = getOverlaySize(appBounds, destinationBounds);
+ mOverlayHalfSize = overlaySize >> 1;
+
+ // When the activity is in the secondary split, make sure the scaling center is not
+ // offset.
+ mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height());
+
+ mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
+ prepareAppIconOverlay(appIcon);
+ mLeash = new SurfaceControl.Builder()
+ .setCallsite(TAG)
+ .setName(LAYER_NAME)
+ .build();
+ }
+
+ /**
+ * Returns the size of the app icon overlay.
+ *
+ * In order to have the overlay always cover the pip window during the transition,
+ * the overlay will be drawn with the max size of the start and end bounds in different
+ * rotation.
+ */
+ public static int getOverlaySize(Rect appBounds, Rect destinationBounds) {
+ final int appWidth = appBounds.width();
+ final int appHeight = appBounds.height();
+
+ return Math.max(Math.max(appWidth, appHeight),
+ Math.max(destinationBounds.width(), destinationBounds.height())) + 1;
+ }
+
+ @Override
+ public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+ tx.show(mLeash);
+ tx.setLayer(mLeash, Integer.MAX_VALUE);
+ tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+ tx.setAlpha(mLeash, 0f);
+ tx.reparent(mLeash, parentLeash);
+ tx.apply();
+ }
+
+ @Override
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ float scale, float fraction, Rect endBounds) {
+ mTmpTransform.reset();
+ // Scale back the bitmap with the pivot at parent origin
+ mTmpTransform.setScale(scale, scale);
+ // We are negative-cropping away from the final bounds crop in config-at-end enter PiP;
+ // this means that the overlay shift depends on the final bounds.
+ // Note: translation is also dependent on the scaling of the parent.
+ mTmpTransform.postTranslate(endBounds.width() / 2f - mOverlayHalfSize * scale,
+ endBounds.height() / 2f - mOverlayHalfSize * scale);
+ atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
+ .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
+ }
+
+
+
+ @Override
+ public void detach(SurfaceControl.Transaction tx) {
+ super.detach(tx);
+ if (mBitmap != null && !mBitmap.isRecycled()) {
+ mBitmap.recycle();
+ }
+ }
+
+ private void prepareAppIconOverlay(Drawable appIcon) {
+ final Canvas canvas = new Canvas();
+ canvas.setBitmap(mBitmap);
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.colorBackground });
+ try {
+ int colorAccent = ta.getColor(0, 0);
+ canvas.drawRGB(
+ Color.red(colorAccent),
+ Color.green(colorAccent),
+ Color.blue(colorAccent));
+ } finally {
+ ta.recycle();
+ }
+ final Rect appIconBounds = new Rect(
+ mOverlayHalfSize - mAppIconSizePx / 2,
+ mOverlayHalfSize - mAppIconSizePx / 2,
+ mOverlayHalfSize + mAppIconSizePx / 2,
+ mOverlayHalfSize + mAppIconSizePx / 2);
+ appIcon.setBounds(appIconBounds);
+ appIcon.draw(canvas);
+ mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 0427294..9a93371 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -456,6 +456,10 @@
}
}
+ private void setLauncherAppIconSize(int iconSizePx) {
+ mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx);
+ }
+
/**
* The interface for calls from outside the Shell, within the host process.
*/
@@ -571,7 +575,10 @@
}
@Override
- public void setLauncherAppIconSize(int iconSizePx) {}
+ public void setLauncherAppIconSize(int iconSizePx) {
+ executeRemoteCallWithTaskPermission(mController, "setLauncherAppIconSize",
+ (controller) -> controller.setLauncherAppIconSize(iconSizePx));
+ }
@Override
public void setPipAnimationListener(IPipAnimationListener listener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index b286211..e90b32c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -30,9 +30,6 @@
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.PictureInPictureParams;
@@ -362,33 +359,19 @@
animator.setEnterStartState(pipChange, pipActivityChange);
animator.onEnterAnimationUpdate(1.0f /* fraction */, startTransaction);
startTransaction.apply();
+
+ if (swipePipToHomeOverlay != null) {
+ // fadeout the overlay if needed.
+ startOverlayFadeoutAnimation(swipePipToHomeOverlay, () -> {
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ tx.remove(swipePipToHomeOverlay);
+ tx.apply();
+ });
+ }
finishInner();
return true;
}
- private void startOverlayFadeoutAnimation() {
- ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f);
- animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
- tx.remove(mPipTransitionState.getSwipePipToHomeOverlay());
- tx.apply();
-
- // We have fully completed enter-PiP animation after the overlay is gone.
- mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
- }
- });
- animator.addUpdateListener(animation -> {
- float alpha = (float) animation.getAnimatedValue();
- SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
- tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply();
- });
- animator.start();
- }
-
private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -405,15 +388,18 @@
return false;
}
- Rect endBounds = pipChange.getEndAbsBounds();
- SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
- Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
+ final Rect startBounds = pipChange.getStartAbsBounds();
+ final Rect endBounds = pipChange.getEndAbsBounds();
- Rect sourceRectHint = null;
- if (pipChange.getTaskInfo() != null
- && pipChange.getTaskInfo().pictureInPictureParams != null) {
- sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint();
- }
+ final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
+ final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params);
+
+ final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds,
+ endBounds);
+
+ final SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+ final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint)
+ : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio);
// For opening type transitions, if there is a change of mode TO_FRONT/OPEN,
// make sure that change has alpha of 1f, since it's init state might be set to alpha=0f
@@ -441,14 +427,36 @@
}
PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
- startTransaction, finishTransaction, endBounds, sourceRectHint, delta);
+ startTransaction, finishTransaction, endBounds, adjustedSourceRectHint, delta);
+ if (sourceRectHint == null) {
+ // update the src-rect-hint in params in place, to set up initial animator transform.
+ params.getSourceRectHint().set(adjustedSourceRectHint);
+ animator.setAppIconContentOverlay(
+ mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo,
+ mPipBoundsState.getLauncherState().getAppIconSizePx());
+ }
animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange,
pipActivityChange));
- animator.setAnimationEndCallback(this::finishInner);
+ animator.setAnimationEndCallback(() -> {
+ if (animator.getContentOverlayLeash() != null) {
+ startOverlayFadeoutAnimation(animator.getContentOverlayLeash(),
+ animator::clearAppIconOverlay);
+ }
+ finishInner();
+ });
animator.start();
return true;
}
+ private void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash,
+ @NonNull Runnable onAnimationEnd) {
+ PipAlphaAnimator animator = new PipAlphaAnimator(mContext, overlayLeash,
+ null /* startTx */, PipAlphaAnimator.FADE_OUT);
+ animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
+ animator.setAnimationEndCallback(onAnimationEnd);
+ animator.start();
+ }
+
private void handleBoundsTypeFixedRotation(TransitionInfo.Change pipTaskChange,
TransitionInfo.Change pipActivityChange, int endRotation) {
final Rect endBounds = pipTaskChange.getEndAbsBounds();
@@ -696,9 +704,7 @@
private void finishInner() {
finishTransition(null /* tx */);
- if (mPipTransitionState.getSwipePipToHomeOverlay() != null) {
- startOverlayFadeoutAnimation();
- } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
+ if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
// If we were entering PiP (i.e. playing the animation) with a valid srcRectHint,
// and then we get a signal on client finishing its draw after the transition
// has ended, then we have fully entered PiP.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index fde01ee..3946b61 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -290,9 +290,11 @@
final Resources res = mResult.mRootView.getResources();
mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */,
- new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res),
- getResizeHandleEdgeInset(res), getFineResizeCornerSize(res),
- getLargeResizeCornerSize(res)), touchSlop);
+ new Size(mResult.mWidth, mResult.mHeight),
+ getResizeEdgeHandleSize(res),
+ getResizeHandleEdgeInset(res), getFineResizeCornerSize(res),
+ getLargeResizeCornerSize(res), DragResizeWindowGeometry.DisabledEdge.NONE),
+ touchSlop);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index f404326..a3324cc6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -187,7 +187,7 @@
new ExclusionRegionListenerImpl();
private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId;
- private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
+ private final DragEventListenerImpl mDragEventListener = new DragEventListenerImpl();
private final InputMonitorFactory mInputMonitorFactory;
private TaskOperations mTaskOperations;
private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
@@ -613,7 +613,8 @@
decoration.mTaskInfo.configuration.windowConfiguration.getBounds(),
left ? SnapPosition.LEFT : SnapPosition.RIGHT,
resizeTrigger,
- motionEvent);
+ motionEvent,
+ mWindowDecorByTaskId.get(taskId));
}
decoration.closeHandleMenu();
@@ -1072,7 +1073,8 @@
taskInfo, decoration.mTaskSurface, position,
new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
newTaskBounds, decoration.calculateValidDragArea(),
- new Rect(mOnDragStartInitialBounds), e);
+ new Rect(mOnDragStartInitialBounds), e,
+ mWindowDecorByTaskId.get(taskInfo.taskId));
if (touchingButton && !mHasLongClicked) {
// We need the input event to not be consumed here to end the ripple
// effect on the touched button. We will reset drag state in the ensuing
@@ -1524,7 +1526,7 @@
mTaskOrganizer,
windowDecoration,
mDisplayController,
- mDragStartListener,
+ mDragEventListener,
mTransitions,
mInteractionJankMonitor,
mTransactionFactory,
@@ -1667,13 +1669,18 @@
}
}
- private class DragStartListenerImpl
- implements DragPositioningCallbackUtility.DragStartListener {
+ private class DragEventListenerImpl
+ implements DragPositioningCallbackUtility.DragEventListener {
@Override
public void onDragStart(int taskId) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
decoration.closeHandleMenu();
}
+
+ @Override
+ public void onDragMove(int taskId) {
+
+ }
}
/**
@@ -1767,7 +1774,7 @@
ShellTaskOrganizer taskOrganizer,
DesktopModeWindowDecoration windowDecoration,
DisplayController displayController,
- DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ DragPositioningCallbackUtility.DragEventListener dragEventListener,
Transitions transitions,
InteractionJankMonitor interactionJankMonitor,
Supplier<SurfaceControl.Transaction> transactionFactory,
@@ -1777,7 +1784,7 @@
taskOrganizer,
windowDecoration,
displayController,
- dragStartListener,
+ dragEventListener,
transitions,
interactionJankMonitor,
handler)
@@ -1786,7 +1793,7 @@
transitions,
windowDecoration,
displayController,
- dragStartListener,
+ dragEventListener,
transactionFactory);
if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 8865112..dc27cfe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -27,14 +27,18 @@
import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
+
import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset;
+import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -155,6 +159,8 @@
private DragResizeInputListener mDragResizeListener;
private Runnable mCurrentViewHostRunnable = null;
private RelayoutParams mRelayoutParams = new RelayoutParams();
+ private DisabledEdge mDisabledResizingEdge =
+ NONE;
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
new WindowDecoration.RelayoutResult<>();
private final Runnable mViewHostRunnable =
@@ -329,6 +335,24 @@
mOnToSplitscreenClickListener = listener;
}
+ /**
+ * Adds a drag resize observer that gets notified on the task being drag resized.
+ *
+ * @param dragResizeListener The observing object to be added.
+ */
+ public void addDragResizeListener(DragEventListener dragResizeListener) {
+ mTaskDragResizer.addDragEventListener(dragResizeListener);
+ }
+
+ /**
+ * Removes an already existing drag resize observer.
+ *
+ * @param dragResizeListener observer to be removed.
+ */
+ public void removeDragResizeListener(DragEventListener dragResizeListener) {
+ mTaskDragResizer.removeDragEventListener(dragResizeListener);
+ }
+
/** Registers a listener to be called when the decoration's new window action is triggered. */
void setOnNewWindowClickListener(Function0<Unit> listener) {
mOnNewWindowClickListener = listener;
@@ -386,6 +410,24 @@
}
}
+ /**
+ * Disables resizing for the given edge.
+ *
+ * @param disabledResizingEdge edge to disable.
+ * @param shouldDelayUpdate whether the update should be executed immediately or delayed.
+ */
+ public void updateDisabledResizingEdge(
+ DragResizeWindowGeometry.DisabledEdge disabledResizingEdge, boolean shouldDelayUpdate) {
+ mDisabledResizingEdge = disabledResizingEdge;
+ final boolean inFullImmersive = mDesktopRepository
+ .isTaskInFullImmersiveState(mTaskInfo.taskId);
+ if (shouldDelayUpdate) {
+ return;
+ }
+ updateDragResizeListener(mDecorationContainerSurface, inFullImmersive);
+ }
+
+
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
@@ -645,7 +687,8 @@
new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius,
new Size(mResult.mWidth, mResult.mHeight),
getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
- getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop)
+ getFineResizeCornerSize(res), getLargeResizeCornerSize(res),
+ mDisabledResizingEdge), touchSlop)
|| !mTaskInfo.positionInParent.equals(mPositionInParent)) {
updateExclusionRegion(inFullImmersive);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
index 01bb7f7..d36fc12 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
@@ -42,7 +42,7 @@
*
* All touch events must be passed through this class to track a drag event.
*/
-class DragDetector {
+public class DragDetector {
private final MotionEventHandler mEventHandler;
private final PointF mInputDownPoint = new PointF();
@@ -55,7 +55,14 @@
private boolean mResultOfDownAction;
- DragDetector(@NonNull MotionEventHandler eventHandler, long holdToDragMinDurationMs,
+ /**
+ * Initialises a drag detector.
+ *
+ * @param eventHandler drag event handler.
+ * @param holdToDragMinDurationMs hold to drag duration.
+ * @param touchSlop touch slope threshold.
+ */
+ public DragDetector(@NonNull MotionEventHandler eventHandler, long holdToDragMinDurationMs,
int touchSlop) {
resetState();
mEventHandler = eventHandler;
@@ -69,7 +76,7 @@
* @return the result returned by {@link #mEventHandler}, or the result when
* {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
*/
- boolean onMotionEvent(MotionEvent ev) {
+ public boolean onMotionEvent(MotionEvent ev) {
return onMotionEvent(null /* view */, ev);
}
@@ -79,7 +86,7 @@
* @return the result returned by {@link #mEventHandler}, or the result when
* {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
*/
- boolean onMotionEvent(View v, MotionEvent ev) {
+ public boolean onMotionEvent(View v, MotionEvent ev) {
final boolean isTouchScreen =
(ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
if (!isTouchScreen) {
@@ -190,7 +197,16 @@
mDidHoldForMinDuration = false;
}
- interface MotionEventHandler {
+ /**
+ * Interface to be implemented by the class using the DragDetector for callback.
+ */
+ public interface MotionEventHandler {
+ /**
+ * Called back when drag is detected to notify the implementing class to handle drag events.
+ * @param v view on which the input arrived.
+ * @param ev motion event that resulted in drag.
+ * @return whether this was a drag event or not.
+ */
boolean handleMotionEvent(@Nullable View v, MotionEvent ev);
}
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 78e7962..8eced3e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -208,7 +208,17 @@
return result;
}
- private static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth,
+ /**
+ * Checks whether the new task bounds exceed the allowed width.
+ *
+ * @param repositionedWidth task width after repositioning.
+ * @param startingWidth task width before repositioning.
+ * @param maxResizeBounds stable bounds for display.
+ * @param displayController display controller for the task being checked.
+ * @param windowDecoration contains decor info and helpers for the task.
+ * @return whether the task is exceeding any of the width constrains, minimum or maximum.
+ */
+ public static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth,
Rect maxResizeBounds, DisplayController displayController,
WindowDecoration windowDecoration) {
boolean isSizeIncreasing = (repositionedWidth - startingWidth) > 0;
@@ -223,7 +233,17 @@
&& repositionedWidth > maxResizeBounds.width() && isSizeIncreasing;
}
- private static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight,
+ /**
+ * Checks whether the new task bounds exceed the allowed height.
+ *
+ * @param repositionedHeight task's height after repositioning.
+ * @param startingHeight task's height before repositioning.
+ * @param maxResizeBounds stable bounds for display.
+ * @param displayController display controller for the task being checked.
+ * @param windowDecoration contains decor info and helpers for the task.
+ * @return whether the task is exceeding any of the height constrains, minimum or maximum.
+ */
+ public static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight,
Rect maxResizeBounds, DisplayController displayController,
WindowDecoration windowDecoration) {
boolean isSizeIncreasing = (repositionedHeight - startingHeight) > 0;
@@ -284,12 +304,19 @@
&& DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS.isTrue();
}
- interface DragStartListener {
+ public interface DragEventListener {
/**
* Inform the implementing class that a drag resize has started
*
* @param taskId id of this positioner's {@link WindowDecoration}
*/
void onDragStart(int taskId);
+
+ /**
+ * Inform the implementing class that a drag move has started.
+ *
+ * @param taskId id of this positioner's {@link WindowDecoration}
+ */
+ void onDragMove(int taskId);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index 844ceb3..6f72d34 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -42,7 +42,7 @@
/**
* Geometry for a drag resize region for a particular window.
*/
-final class DragResizeWindowGeometry {
+public final class DragResizeWindowGeometry {
private final int mTaskCornerRadius;
private final Size mTaskSize;
// The size of the handle outside the task window applied to the edges of the window, for the
@@ -58,19 +58,24 @@
// The bounds for each edge drag region, which can resize the task in one direction.
final @NonNull TaskEdges mTaskEdges;
+ private final DisabledEdge mDisabledEdge;
+
DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize,
int resizeHandleEdgeOutset, int resizeHandleEdgeInset, int fineCornerSize,
- int largeCornerSize) {
+ int largeCornerSize, DisabledEdge disabledEdge) {
mTaskCornerRadius = taskCornerRadius;
mTaskSize = taskSize;
mResizeHandleEdgeOutset = resizeHandleEdgeOutset;
mResizeHandleEdgeInset = resizeHandleEdgeInset;
- mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize);
- mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize);
+ mDisabledEdge = disabledEdge;
+
+ mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize, disabledEdge);
+ mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize, disabledEdge);
// Save touch areas for each edge.
- mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset);
+ mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset,
+ mDisabledEdge);
}
/**
@@ -170,7 +175,7 @@
|| e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE
// Touchpad input
|| (e.isFromSource(SOURCE_MOUSE)
- && e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER);
+ && e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER);
} else {
return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
}
@@ -187,8 +192,9 @@
/**
* Returns the control type for the drag-resize, based on the touch regions and this
* MotionEvent's coordinates.
+ *
* @param isTouchscreen Controls the size of the corner resize regions; touchscreen events
- * (finger & stylus) are eligible for a larger area than cursor events
+ * (finger & stylus) are eligible for a larger area than cursor events.
* @param isEdgeResizePermitted Indicates if the event is eligible for falling into an edge
* resize region.
*/
@@ -252,6 +258,10 @@
@DragPositioningCallback.CtrlType
private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, float x,
float y) {
+ if ((mDisabledEdge == DisabledEdge.RIGHT && (ctrlType & CTRL_TYPE_RIGHT) != 0)
+ || mDisabledEdge == DisabledEdge.LEFT && ((ctrlType & CTRL_TYPE_LEFT) != 0)) {
+ return CTRL_TYPE_UNDEFINED;
+ }
final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType);
double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y);
@@ -337,29 +347,31 @@
private final @NonNull Rect mRightTopCornerBounds;
private final @NonNull Rect mLeftBottomCornerBounds;
private final @NonNull Rect mRightBottomCornerBounds;
+ private final @NonNull DisabledEdge mDisabledEdge;
- TaskCorners(@NonNull Size taskSize, int cornerSize) {
+ TaskCorners(@NonNull Size taskSize, int cornerSize, DisabledEdge disabledEdge) {
mCornerSize = cornerSize;
+ mDisabledEdge = disabledEdge;
final int cornerRadius = cornerSize / 2;
- mLeftTopCornerBounds = new Rect(
+ mLeftTopCornerBounds = (disabledEdge == DisabledEdge.LEFT) ? new Rect() : new Rect(
-cornerRadius,
-cornerRadius,
cornerRadius,
cornerRadius);
- mRightTopCornerBounds = new Rect(
+ mRightTopCornerBounds = (disabledEdge == DisabledEdge.RIGHT) ? new Rect() : new Rect(
taskSize.getWidth() - cornerRadius,
-cornerRadius,
taskSize.getWidth() + cornerRadius,
cornerRadius);
- mLeftBottomCornerBounds = new Rect(
+ mLeftBottomCornerBounds = (disabledEdge == DisabledEdge.LEFT) ? new Rect() : new Rect(
-cornerRadius,
taskSize.getHeight() - cornerRadius,
cornerRadius,
taskSize.getHeight() + cornerRadius);
- mRightBottomCornerBounds = new Rect(
+ mRightBottomCornerBounds = (disabledEdge == DisabledEdge.RIGHT) ? new Rect() : new Rect(
taskSize.getWidth() - cornerRadius,
taskSize.getHeight() - cornerRadius,
taskSize.getWidth() + cornerRadius,
@@ -370,10 +382,14 @@
* Updates the region to include all four corners.
*/
void union(Region region) {
- region.union(mLeftTopCornerBounds);
- region.union(mRightTopCornerBounds);
- region.union(mLeftBottomCornerBounds);
- region.union(mRightBottomCornerBounds);
+ if (mDisabledEdge != DisabledEdge.RIGHT) {
+ region.union(mRightTopCornerBounds);
+ region.union(mRightBottomCornerBounds);
+ }
+ if (mDisabledEdge != DisabledEdge.LEFT) {
+ region.union(mLeftTopCornerBounds);
+ region.union(mLeftBottomCornerBounds);
+ }
}
/**
@@ -440,9 +456,12 @@
private final @NonNull Rect mRightEdgeBounds;
private final @NonNull Rect mBottomEdgeBounds;
private final @NonNull Region mRegion;
+ private final @NonNull DisabledEdge mDisabledEdge;
private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness,
- int resizeHandleEdgeInset) {
+ int resizeHandleEdgeInset, DisabledEdge disabledEdge) {
+ // Save touch areas for each edge.
+ mDisabledEdge = disabledEdge;
// Save touch areas for each edge.
mTopEdgeBounds = new Rect(
-resizeHandleThickness,
@@ -466,10 +485,7 @@
taskSize.getHeight() + resizeHandleThickness);
mRegion = new Region();
- mRegion.union(mTopEdgeBounds);
- mRegion.union(mLeftEdgeBounds);
- mRegion.union(mRightEdgeBounds);
- mRegion.union(mBottomEdgeBounds);
+ union(mRegion);
}
/**
@@ -483,9 +499,13 @@
* Updates the region to include all four corners.
*/
private void union(Region region) {
+ if (mDisabledEdge != DisabledEdge.RIGHT) {
+ region.union(mRightEdgeBounds);
+ }
+ if (mDisabledEdge != DisabledEdge.LEFT) {
+ region.union(mLeftEdgeBounds);
+ }
region.union(mTopEdgeBounds);
- region.union(mLeftEdgeBounds);
- region.union(mRightEdgeBounds);
region.union(mBottomEdgeBounds);
}
@@ -519,4 +539,10 @@
mBottomEdgeBounds);
}
}
+
+ public enum DisabledEdge {
+ LEFT,
+ RIGHT,
+ NONE
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index ccf329c..3efae9d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -35,6 +35,7 @@
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.transition.Transitions;
+import java.util.ArrayList;
import java.util.function.Supplier;
/**
@@ -55,7 +56,8 @@
private final WindowDecoration mWindowDecoration;
private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
private DisplayController mDisplayController;
- private DragPositioningCallbackUtility.DragStartListener mDragStartListener;
+ private ArrayList<DragPositioningCallbackUtility.DragEventListener> mDragEventListeners =
+ new ArrayList<>();
private final Rect mStableBounds = new Rect();
private final Rect mTaskBoundsAtDragStart = new Rect();
private final PointF mRepositionStartPoint = new PointF();
@@ -69,20 +71,22 @@
FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, Transitions transitions,
WindowDecoration windowDecoration, DisplayController displayController) {
this(taskOrganizer, transitions, windowDecoration, displayController,
- dragStartListener -> {}, SurfaceControl.Transaction::new);
+ null, SurfaceControl.Transaction::new);
}
FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
Transitions transitions,
WindowDecoration windowDecoration,
DisplayController displayController,
- DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ DragPositioningCallbackUtility.DragEventListener dragEventListener,
Supplier<SurfaceControl.Transaction> supplier) {
mTaskOrganizer = taskOrganizer;
mTransitions = transitions;
mWindowDecoration = windowDecoration;
mDisplayController = displayController;
- mDragStartListener = dragStartListener;
+ if (dragEventListener != null) {
+ mDragEventListeners.add(dragEventListener);
+ }
mTransactionSupplier = supplier;
}
@@ -92,7 +96,9 @@
mTaskBoundsAtDragStart.set(
mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
mRepositionStartPoint.set(x, y);
- mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
+ for (DragPositioningCallbackUtility.DragEventListener listener : mDragEventListeners) {
+ listener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
+ }
if (mCtrlType != CTRL_TYPE_UNDEFINED && !mWindowDecoration.mHasGlobalFocus) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mWindowDecoration.mTaskInfo.token, true /* onTop */,
@@ -120,6 +126,10 @@
// The task is being resized, send the |dragResizing| hint to core with the first
// bounds-change wct.
if (!mHasDragResized) {
+ for (DragPositioningCallbackUtility.DragEventListener listener :
+ mDragEventListeners) {
+ listener.onDragMove(mWindowDecoration.mTaskInfo.taskId);
+ }
// This is the first bounds change since drag resize operation started.
wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
}
@@ -216,4 +226,16 @@
public boolean isResizingOrAnimating() {
return mIsResizingOrAnimatingResize;
}
+
+ @Override
+ public void addDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+ mDragEventListeners.add(dragEventListener);
+ }
+
+ @Override
+ public void removeDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+ mDragEventListeners.remove(dragEventListener);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index fb81ed4..8770d35 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -49,7 +49,7 @@
/**
* Creates and updates a veil that covers task contents on resize.
*/
-class ResizeVeil @JvmOverloads constructor(
+public class ResizeVeil @JvmOverloads constructor(
private val context: Context,
private val displayController: DisplayController,
private val appIcon: Bitmap,
@@ -188,28 +188,16 @@
t.apply()
return
}
- isVisible = true
val background = backgroundSurface
val icon = iconSurface
- val veil = veilSurface
- if (background == null || icon == null || veil == null) return
-
- // Parent surface can change, ensure it is up to date.
- if (parent != parentSurface) {
- t.reparent(veil, parent)
- parentSurface = parent
- }
-
- val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) {
- Theme.LIGHT -> lightColors.surfaceContainer
- Theme.DARK -> darkColors.surfaceContainer
- }
- t.show(veil)
- .setLayer(veil, VEIL_CONTAINER_LAYER)
- .setLayer(icon, VEIL_ICON_LAYER)
- .setLayer(background, VEIL_BACKGROUND_LAYER)
- .setColor(background, Color.valueOf(backgroundColor.toArgb()).components)
- relayout(taskBounds, t)
+ if (background == null || icon == null) return
+ updateTransactionWithShowVeil(
+ t,
+ parent,
+ taskBounds,
+ taskInfo,
+ fadeIn,
+ )
if (fadeIn) {
cancelAnimation()
val veilAnimT = surfaceControlTransactionSupplier.get()
@@ -259,11 +247,43 @@
iconAnimator.start()
} else {
// Show the veil immediately.
+ t.apply()
+ }
+ }
+
+ fun updateTransactionWithShowVeil(
+ t: SurfaceControl.Transaction,
+ parent: SurfaceControl,
+ taskBounds: Rect,
+ taskInfo: RunningTaskInfo,
+ fadeIn: Boolean = false,
+ ) {
+ if (!isReady || isVisible) return
+ isVisible = true
+ val background = backgroundSurface
+ val icon = iconSurface
+ val veil = veilSurface
+ if (background == null || icon == null || veil == null) return
+ // Parent surface can change, ensure it is up to date.
+ if (parent != parentSurface) {
+ t.reparent(veil, parent)
+ parentSurface = parent
+ }
+ val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) {
+ Theme.LIGHT -> lightColors.surfaceContainer
+ Theme.DARK -> darkColors.surfaceContainer
+ }
+ t.show(veil)
+ .setLayer(veil, VEIL_CONTAINER_LAYER)
+ .setLayer(icon, VEIL_ICON_LAYER)
+ .setLayer(background, VEIL_BACKGROUND_LAYER)
+ .setColor(background, Color.valueOf(backgroundColor.toArgb()).components)
+ relayout(taskBounds, t)
+ if (!fadeIn) {
t.show(icon)
- .show(background)
- .setAlpha(icon, 1f)
- .setAlpha(background, 1f)
- .apply()
+ .show(background)
+ .setAlpha(icon, 1f)
+ .setAlpha(background, 1f)
}
}
@@ -314,8 +334,12 @@
* @param newBounds bounds to update veil to.
*/
fun updateResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) {
+ updateTransactionWithResizeVeil(t, newBounds)
+ t.apply()
+ }
+
+ fun updateTransactionWithResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) {
if (!isVisible) {
- t.apply()
return
}
veilAnimator?.let { animator ->
@@ -325,7 +349,6 @@
}
}
relayout(newBounds, t)
- t.apply()
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
index d7ea0c3..63b288d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
@@ -26,4 +26,19 @@
* a resize is complete.
*/
boolean isResizingOrAnimating();
+
+ /**
+ * Adds a drag start listener to be notified of drag start events.
+ *
+ * @param dragEventListener Listener to be added.
+ */
+ void addDragEventListener(DragPositioningCallbackUtility.DragEventListener dragEventListener);
+
+ /**
+ * Removes a drag start listener from the listener set.
+ *
+ * @param dragEventListener Listener to be removed.
+ */
+ void removeDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index ff3b455..a1e329a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -43,6 +43,7 @@
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.transition.Transitions;
+import java.util.ArrayList;
import java.util.function.Supplier;
/**
@@ -56,7 +57,8 @@
private DesktopModeWindowDecoration mDesktopWindowDecoration;
private ShellTaskOrganizer mTaskOrganizer;
private DisplayController mDisplayController;
- private DragPositioningCallbackUtility.DragStartListener mDragStartListener;
+ private ArrayList<DragPositioningCallbackUtility.DragEventListener>
+ mDragEventListeners = new ArrayList<>();
private final Transitions mTransitions;
private final Rect mStableBounds = new Rect();
private final Rect mTaskBoundsAtDragStart = new Rect();
@@ -73,23 +75,23 @@
public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
DesktopModeWindowDecoration windowDecoration,
DisplayController displayController,
- DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ DragPositioningCallbackUtility.DragEventListener dragEventListener,
Transitions transitions, InteractionJankMonitor interactionJankMonitor,
@ShellMainThread Handler handler) {
- this(taskOrganizer, windowDecoration, displayController, dragStartListener,
+ this(taskOrganizer, windowDecoration, displayController, dragEventListener,
SurfaceControl.Transaction::new, transitions, interactionJankMonitor, handler);
}
public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
DesktopModeWindowDecoration windowDecoration,
DisplayController displayController,
- DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ DragPositioningCallbackUtility.DragEventListener dragEventListener,
Supplier<SurfaceControl.Transaction> supplier, Transitions transitions,
InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler) {
mDesktopWindowDecoration = windowDecoration;
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
- mDragStartListener = dragStartListener;
+ mDragEventListeners.add(dragEventListener);
mTransactionSupplier = supplier;
mTransitions = transitions;
mInteractionJankMonitor = interactionJankMonitor;
@@ -113,7 +115,10 @@
mTaskOrganizer.applyTransaction(wct);
}
}
- mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
+ for (DragPositioningCallbackUtility.DragEventListener dragEventListener :
+ mDragEventListeners) {
+ dragEventListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
+ }
mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
int rotation = mDesktopWindowDecoration
.mTaskInfo.configuration.windowConfiguration.getDisplayRotation();
@@ -137,6 +142,10 @@
mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta,
mDisplayController, mDesktopWindowDecoration)) {
if (!mIsResizingOrAnimatingResize) {
+ for (DragPositioningCallbackUtility.DragEventListener dragEventListener :
+ mDragEventListeners) {
+ dragEventListener.onDragMove(mDesktopWindowDecoration.mTaskInfo.taskId);
+ }
mDesktopWindowDecoration.showResizeVeil(mRepositionTaskBounds);
mIsResizingOrAnimatingResize = true;
} else {
@@ -237,4 +246,16 @@
public boolean isResizingOrAnimating() {
return mIsResizingOrAnimatingResize;
}
+
+ @Override
+ public void addDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+ mDragEventListeners.add(dragEventListener);
+ }
+
+ @Override
+ public void removeDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+ mDragEventListeners.remove(dragEventListener);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 34cc098..f97dfb89 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -131,12 +131,15 @@
}
};
- RunningTaskInfo mTaskInfo;
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public RunningTaskInfo mTaskInfo;
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public Context mDecorWindowContext;
int mLayoutResId;
- final SurfaceControl mTaskSurface;
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public final SurfaceControl mTaskSurface;
Display mDisplay;
- Context mDecorWindowContext;
SurfaceControl mDecorationContainerSurface;
SurfaceControl mCaptionContainerSurface;
@@ -200,6 +203,14 @@
}
/**
+ * Gets the decoration's task leash.
+ * @return the decoration' task surface used to manipulate the task.
+ */
+ public SurfaceControl getLeash() {
+ return mTaskSurface;
+ }
+
+ /**
* Used by {@link WindowDecoration} to trigger a new relayout because the requirements for a
* relayout weren't satisfied are satisfied now.
*
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
new file mode 100644
index 0000000..ff418c6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.graphics.Rect
+import android.util.SparseArray
+import androidx.core.util.valueIterator
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+
+/** Manages tiling for each displayId/userId independently. */
+class DesktopTilingDecorViewModel(
+ private val context: Context,
+ private val displayController: DisplayController,
+ private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val syncQueue: SyncTransactionQueue,
+ private val transitions: Transitions,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
+ private val returnToDragStartAnimator: ReturnToDragStartAnimator,
+ private val taskRepository: DesktopRepository,
+) {
+ @VisibleForTesting
+ var tilingTransitionHandlerByDisplayId = SparseArray<DesktopTilingWindowDecoration>()
+
+ fun snapToHalfScreen(
+ taskInfo: ActivityManager.RunningTaskInfo,
+ desktopModeWindowDecoration: DesktopModeWindowDecoration,
+ position: DesktopTasksController.SnapPosition,
+ destinationBounds: Rect,
+ ): Boolean {
+ val displayId = taskInfo.displayId
+ val handler =
+ tilingTransitionHandlerByDisplayId.get(displayId)
+ ?: run {
+ val newHandler =
+ DesktopTilingWindowDecoration(
+ context,
+ syncQueue,
+ displayController,
+ displayId,
+ rootTdaOrganizer,
+ transitions,
+ shellTaskOrganizer,
+ toggleResizeDesktopTaskTransitionHandler,
+ returnToDragStartAnimator,
+ taskRepository,
+ )
+ tilingTransitionHandlerByDisplayId.put(displayId, newHandler)
+ newHandler
+ }
+ transitions.registerObserver(handler)
+ return handler.onAppTiled(
+ taskInfo,
+ desktopModeWindowDecoration,
+ position,
+ destinationBounds,
+ )
+ }
+
+ fun removeTaskIfTiled(displayId: Int, taskId: Int) {
+ tilingTransitionHandlerByDisplayId.get(displayId)?.removeTaskIfTiled(taskId)
+ }
+
+ fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean {
+ return tilingTransitionHandlerByDisplayId
+ .get(taskInfo.displayId)
+ ?.moveTiledPairToFront(taskInfo) ?: false
+ }
+
+ fun onOverviewAnimationStateChange(isRunning: Boolean) {
+ for (tilingHandler in tilingTransitionHandlerByDisplayId.valueIterator()) {
+ tilingHandler.onOverviewAnimationStateChange(isRunning)
+ }
+ }
+
+ fun onUserChange() {
+ for (tilingHandler in tilingTransitionHandlerByDisplayId.valueIterator()) {
+ tilingHandler.onUserChange()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
new file mode 100644
index 0000000..9bf1304
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.graphics.Region
+import android.os.Binder
+import android.view.LayoutInflater
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+import android.view.WindowManager.LayoutParams.FLAG_SLIPPERY
+import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER
+import android.view.WindowlessWindowManager
+import com.android.wm.shell.R
+import com.android.wm.shell.common.SyncTransactionQueue
+import java.util.function.Supplier
+
+/**
+ * a [WindowlessWindowManaer] responsible for hosting the [TilingDividerView] on the display root
+ * when two tasks are tiled on left and right to resize them simultaneously.
+ */
+class DesktopTilingDividerWindowManager(
+ private val config: Configuration,
+ private val windowName: String,
+ private val context: Context,
+ private val leash: SurfaceControl,
+ private val syncQueue: SyncTransactionQueue,
+ private val transitionHandler: DesktopTilingWindowDecoration,
+ private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
+ private var dividerBounds: Rect,
+) : WindowlessWindowManager(config, leash, null), DividerMoveCallback, View.OnLayoutChangeListener {
+ private lateinit var viewHost: SurfaceControlViewHost
+ private var tilingDividerView: TilingDividerView? = null
+ private var dividerShown = false
+ private var handleRegionWidth: Int = -1
+ private var setTouchRegion = true
+
+ /**
+ * Gets bounds of divider window with screen based coordinate on the param Rect.
+ *
+ * @param rect bounds for the [TilingDividerView]
+ */
+ fun getDividerBounds(rect: Rect) {
+ rect.set(dividerBounds)
+ }
+
+ /** Sets the touch region for the SurfaceControlViewHost. */
+ fun setTouchRegion(region: Rect) {
+ setTouchRegion(viewHost.windowToken.asBinder(), Region(region))
+ }
+
+ /**
+ * Builds a view host upon tiling two tasks left and right, and shows the divider view in the
+ * middle of the screen between both tasks.
+ *
+ * @param relativeLeash the task leash that the TilingDividerView should be shown on top of.
+ */
+ fun generateViewHost(relativeLeash: SurfaceControl) {
+ val t = transactionSupplier.get()
+ val surfaceControlViewHost =
+ SurfaceControlViewHost(context, context.display, this, "DesktopTilingManager")
+ val dividerView =
+ LayoutInflater.from(context).inflate(R.layout.tiling_split_divider, /* root= */ null)
+ as TilingDividerView
+ val lp = getWindowManagerParams()
+ surfaceControlViewHost.setView(dividerView, lp)
+ val tmpDividerBounds = Rect()
+ getDividerBounds(tmpDividerBounds)
+ dividerView.setup(this, tmpDividerBounds)
+ t.setRelativeLayer(leash, relativeLeash, 1)
+ .setPosition(leash, dividerBounds.left.toFloat(), dividerBounds.top.toFloat())
+ .show(leash)
+ syncQueue.runInSync { transaction ->
+ transaction.merge(t)
+ t.close()
+ }
+ dividerShown = true
+ viewHost = surfaceControlViewHost
+ dividerView.addOnLayoutChangeListener(this)
+ tilingDividerView = dividerView
+ handleRegionWidth = dividerView.handleRegionWidth
+ }
+
+ /** Hides the divider bar. */
+ fun hideDividerBar() {
+ if (!dividerShown) {
+ return
+ }
+ val t = transactionSupplier.get()
+ t.hide(leash)
+ t.apply()
+ dividerShown = false
+ }
+
+ /** Shows the divider bar. */
+ fun showDividerBar() {
+ if (dividerShown) return
+ val t = transactionSupplier.get()
+ t.show(leash)
+ t.apply()
+ dividerShown = true
+ }
+
+ /**
+ * When the tiled task on top changes, the divider bar's Z access should change to be on top of
+ * the latest focused task.
+ */
+ fun onRelativeLeashChanged(relativeLeash: SurfaceControl, t: SurfaceControl.Transaction) {
+ t.setRelativeLayer(leash, relativeLeash, 1)
+ }
+
+ override fun onDividerMoveStart(pos: Int) {
+ setSlippery(false)
+ }
+
+ /**
+ * Moves the divider view to a new position after touch, gets called from the
+ * [TilingDividerView] onTouch function.
+ */
+ override fun onDividerMove(pos: Int): Boolean {
+ val t = transactionSupplier.get()
+ t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat())
+ val dividerWidth = dividerBounds.width()
+ dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom)
+ return transitionHandler.onDividerHandleMoved(dividerBounds, t)
+ }
+
+ /**
+ * Notifies the transition handler of tiling operations ending, which might result in resizing
+ * WindowContainerTransactions if the sizes of the tiled tasks changed.
+ */
+ override fun onDividerMovedEnd(pos: Int) {
+ setSlippery(true)
+ val t = transactionSupplier.get()
+ t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat())
+ val dividerWidth = dividerBounds.width()
+ dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom)
+ transitionHandler.onDividerHandleDragEnd(dividerBounds, t)
+ }
+
+ private fun getWindowManagerParams(): WindowManager.LayoutParams {
+ val lp =
+ WindowManager.LayoutParams(
+ dividerBounds.width(),
+ dividerBounds.height(),
+ TYPE_DOCK_DIVIDER,
+ FLAG_NOT_FOCUSABLE or
+ FLAG_NOT_TOUCH_MODAL or
+ FLAG_WATCH_OUTSIDE_TOUCH or
+ FLAG_SPLIT_TOUCH or
+ FLAG_SLIPPERY,
+ PixelFormat.TRANSLUCENT,
+ )
+ lp.token = Binder()
+ lp.title = windowName
+ lp.privateFlags =
+ lp.privateFlags or (PRIVATE_FLAG_NO_MOVE_ANIMATION or PRIVATE_FLAG_TRUSTED_OVERLAY)
+ return lp
+ }
+
+ /**
+ * Releases the surface control of the current [TilingDividerView] and tear down the view
+ * hierarchy.y.
+ */
+ fun release() {
+ tilingDividerView = null
+ viewHost.release()
+ transactionSupplier.get().hide(leash).remove(leash).apply()
+ }
+
+ override fun onLayoutChange(
+ v: View?,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int,
+ ) {
+ if (!setTouchRegion) return
+
+ val startX = (dividerBounds.width() - handleRegionWidth) / 2
+ val startY = 0
+ val tempRect = Rect(startX, startY, startX + handleRegionWidth, dividerBounds.height())
+ setTouchRegion(tempRect)
+ setTouchRegion = false
+ }
+
+ private fun setSlippery(slippery: Boolean) {
+ val lp = tilingDividerView?.layoutParams as WindowManager.LayoutParams
+ val isSlippery = (lp.flags and FLAG_SLIPPERY) != 0
+ if (isSlippery == slippery) return
+
+ if (slippery) {
+ lp.flags = lp.flags or FLAG_SLIPPERY
+ } else {
+ lp.flags = lp.flags and FLAG_SLIPPERY.inv()
+ }
+ viewHost.relayout(lp)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
new file mode 100644
index 0000000..6ea1d14
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
@@ -0,0 +1,654 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.os.IBinder
+import android.util.Slog
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.internal.annotations.VisibleForTesting
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT
+import com.android.launcher3.icons.IconProvider
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MINIMIZE
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
+import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE
+import com.android.wm.shell.windowdecor.ResizeVeil
+import com.android.wm.shell.windowdecor.extension.isFullscreen
+import java.util.function.Supplier
+
+class DesktopTilingWindowDecoration(
+ private var context: Context,
+ private val syncQueue: SyncTransactionQueue,
+ private val displayController: DisplayController,
+ private val displayId: Int,
+ private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val transitions: Transitions,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
+ private val returnToDragStartAnimator: ReturnToDragStartAnimator,
+ private val taskRepository: DesktopRepository,
+ private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
+) :
+ Transitions.TransitionHandler,
+ ShellTaskOrganizer.FocusListener,
+ ShellTaskOrganizer.TaskVanishedListener,
+ DragEventListener,
+ Transitions.TransitionObserver {
+ companion object {
+ private val TAG: String = DesktopTilingWindowDecoration::class.java.simpleName
+ private const val TILING_DIVIDER_TAG = "Tiling Divider"
+ }
+
+ var leftTaskResizingHelper: AppResizingHelper? = null
+ var rightTaskResizingHelper: AppResizingHelper? = null
+ private var isTilingManagerInitialised = false
+ @VisibleForTesting
+ var desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager? = null
+ private lateinit var dividerBounds: Rect
+ private var isResizing = false
+ private var isTilingFocused = false
+
+ fun onAppTiled(
+ taskInfo: RunningTaskInfo,
+ desktopModeWindowDecoration: DesktopModeWindowDecoration,
+ position: SnapPosition,
+ currentBounds: Rect,
+ ): Boolean {
+ val destinationBounds = getSnapBounds(taskInfo, position)
+ val resizeMetadata =
+ AppResizingHelper(
+ taskInfo,
+ desktopModeWindowDecoration,
+ context,
+ destinationBounds,
+ displayController,
+ transactionSupplier,
+ )
+ val isFirstTiledApp = leftTaskResizingHelper == null && rightTaskResizingHelper == null
+ val isTiled = destinationBounds != taskInfo.configuration.windowConfiguration.bounds
+
+ initTilingApps(resizeMetadata, position, taskInfo)
+ // Observe drag resizing to break tiling if a task is drag resized.
+ desktopModeWindowDecoration.addDragResizeListener(this)
+
+ if (isTiled) {
+ val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
+ toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentBounds)
+ } else {
+ // Handle the case where we attempt to snap resize when already snap resized: the task
+ // position won't need to change but we want to animate the surface going back to the
+ // snapped position from the "dragged-to-the-edge" position.
+ if (destinationBounds != currentBounds) {
+ returnToDragStartAnimator.start(
+ taskInfo.taskId,
+ resizeMetadata.getLeash(),
+ startBounds = currentBounds,
+ endBounds = destinationBounds,
+ isResizable = taskInfo.isResizeable,
+ )
+ }
+ }
+ initTilingForDisplayIfNeeded(taskInfo.configuration, isFirstTiledApp)
+ return isTiled
+ }
+
+ // If a task is already tiled on the same position, release this task, otherwise if the same
+ // task is tiled on the opposite side, remove it from the opposite side so it's tiled correctly.
+ private fun initTilingApps(
+ taskResizingHelper: AppResizingHelper,
+ position: SnapPosition,
+ taskInfo: RunningTaskInfo,
+ ) {
+ when (position) {
+ SnapPosition.RIGHT -> {
+ rightTaskResizingHelper?.let { removeTaskIfTiled(it.taskInfo.taskId) }
+ if (leftTaskResizingHelper?.taskInfo?.taskId == taskInfo.taskId) {
+ removeTaskIfTiled(taskInfo.taskId)
+ }
+ rightTaskResizingHelper = taskResizingHelper
+ }
+
+ SnapPosition.LEFT -> {
+ leftTaskResizingHelper?.let { removeTaskIfTiled(it.taskInfo.taskId) }
+ if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+ removeTaskIfTiled(taskInfo.taskId)
+ }
+ leftTaskResizingHelper = taskResizingHelper
+ }
+ }
+ }
+
+ private fun initTilingForDisplayIfNeeded(config: Configuration, firstTiledApp: Boolean) {
+ if (leftTaskResizingHelper != null && rightTaskResizingHelper != null) {
+ if (!isTilingManagerInitialised) {
+ desktopTilingDividerWindowManager = initTilingManagerForDisplay(displayId, config)
+ isTilingManagerInitialised = true
+ shellTaskOrganizer.addFocusListener(this)
+ isTilingFocused = true
+ }
+ leftTaskResizingHelper?.initIfNeeded()
+ rightTaskResizingHelper?.initIfNeeded()
+ leftTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(
+ DragResizeWindowGeometry.DisabledEdge.RIGHT,
+ /* shouldDelayUpdate = */ false,
+ )
+ rightTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(
+ DragResizeWindowGeometry.DisabledEdge.LEFT,
+ /* shouldDelayUpdate = */ false,
+ )
+ } else if (firstTiledApp) {
+ shellTaskOrganizer.addTaskVanishedListener(this)
+ }
+ }
+
+ private fun initTilingManagerForDisplay(
+ displayId: Int,
+ config: Configuration,
+ ): DesktopTilingDividerWindowManager? {
+ val displayLayout = displayController.getDisplayLayout(displayId)
+ val builder = SurfaceControl.Builder()
+ rootTdaOrganizer.attachToDisplayArea(displayId, builder)
+ val leash = builder.setName(TILING_DIVIDER_TAG).setContainerLayer().build()
+ val tilingManager =
+ displayLayout?.let {
+ dividerBounds = inflateDividerBounds(it)
+ DesktopTilingDividerWindowManager(
+ config,
+ TAG,
+ context,
+ leash,
+ syncQueue,
+ this,
+ transactionSupplier,
+ dividerBounds,
+ )
+ }
+ // a leash to present the divider on top of, without re-parenting.
+ val relativeLeash =
+ leftTaskResizingHelper?.desktopModeWindowDecoration?.getLeash() ?: return tilingManager
+ tilingManager?.generateViewHost(relativeLeash)
+ return tilingManager
+ }
+
+ fun onDividerHandleMoved(dividerBounds: Rect, t: SurfaceControl.Transaction): Boolean {
+ val leftTiledTask = leftTaskResizingHelper ?: return false
+ val rightTiledTask = rightTaskResizingHelper ?: return false
+ val stableBounds = Rect()
+ val displayLayout = displayController.getDisplayLayout(displayId)
+ displayLayout?.getStableBounds(stableBounds)
+
+ if (stableBounds.isEmpty) return false
+
+ val leftBounds = leftTiledTask.bounds
+ val rightBounds = rightTiledTask.bounds
+ val newLeftBounds =
+ Rect(leftBounds.left, leftBounds.top, dividerBounds.left, leftBounds.bottom)
+ val newRightBounds =
+ Rect(dividerBounds.right, rightBounds.top, rightBounds.right, rightBounds.bottom)
+
+ // If one of the apps is getting smaller or bigger than size constraint, ignore finger move.
+ if (
+ isResizeWithinSizeConstraints(
+ newLeftBounds,
+ newRightBounds,
+ leftBounds,
+ rightBounds,
+ stableBounds,
+ )
+ ) {
+ return false
+ }
+
+ // The final new bounds for each app has to be registered to make sure a startAnimate
+ // when the new bounds are different from old bounds, otherwise hide the veil without
+ // waiting for an animation as no animation will run when no bounds are changed.
+ leftTiledTask.newBounds.set(newLeftBounds)
+ rightTiledTask.newBounds.set(newRightBounds)
+ if (!isResizing) {
+ leftTiledTask.showVeil(t)
+ rightTiledTask.showVeil(t)
+ isResizing = true
+ } else {
+ leftTiledTask.updateVeil(t)
+ rightTiledTask.updateVeil(t)
+ }
+
+ // Applies showing/updating veil for both apps and moving the divider into its new position.
+ t.apply()
+ return true
+ }
+
+ fun onDividerHandleDragEnd(dividerBounds: Rect, t: SurfaceControl.Transaction) {
+ val leftTiledTask = leftTaskResizingHelper ?: return
+ val rightTiledTask = rightTaskResizingHelper ?: return
+
+ if (leftTiledTask.newBounds == leftTiledTask.bounds) {
+ leftTiledTask.hideVeil()
+ rightTiledTask.hideVeil()
+ isResizing = false
+ return
+ }
+ leftTiledTask.bounds.set(leftTiledTask.newBounds)
+ rightTiledTask.bounds.set(rightTiledTask.newBounds)
+ onDividerHandleMoved(dividerBounds, t)
+ isResizing = false
+ val wct = WindowContainerTransaction()
+ wct.setBounds(leftTiledTask.taskInfo.token, leftTiledTask.bounds)
+ wct.setBounds(rightTiledTask.taskInfo.token, rightTiledTask.bounds)
+ transitions.startTransition(TRANSIT_CHANGE, wct, this)
+ }
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ val leftTiledTask = leftTaskResizingHelper ?: return false
+ val rightTiledTask = rightTaskResizingHelper ?: return false
+ for (change in info.getChanges()) {
+ val sc: SurfaceControl = change.getLeash()
+ val endBounds =
+ if (change.taskInfo?.taskId == leftTiledTask.taskInfo.taskId) {
+ leftTiledTask.bounds
+ } else {
+ rightTiledTask.bounds
+ }
+ startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+ finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+ }
+
+ startTransaction.apply()
+ leftTiledTask.hideVeil()
+ rightTiledTask.hideVeil()
+ finishCallback.onTransitionFinished(null)
+ return true
+ }
+
+ // TODO(b/361505243) bring tasks to front here when the empty request info bug is fixed.
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? {
+ return null
+ }
+
+ override fun onDragStart(taskId: Int) {}
+
+ override fun onDragMove(taskId: Int) {
+ removeTaskIfTiled(taskId)
+ }
+
+ override fun onTransitionReady(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ ) {
+ for (change in info.changes) {
+ change.taskInfo?.let {
+ if (it.isFullscreen || isMinimized(change.mode, info.type)) {
+ removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen)
+ }
+ }
+ }
+ }
+
+ private fun isMinimized(changeMode: Int, infoType: Int): Boolean {
+ return (changeMode == TRANSIT_TO_BACK &&
+ (infoType == TRANSIT_MINIMIZE || infoType == TRANSIT_TO_BACK))
+ }
+
+ class AppResizingHelper(
+ val taskInfo: RunningTaskInfo,
+ val desktopModeWindowDecoration: DesktopModeWindowDecoration,
+ val context: Context,
+ val bounds: Rect,
+ val displayController: DisplayController,
+ val transactionSupplier: Supplier<Transaction>,
+ ) {
+ var isInitialised = false
+ var newBounds = Rect(bounds)
+ private lateinit var resizeVeilBitmap: Bitmap
+ private lateinit var resizeVeil: ResizeVeil
+ private val displayContext = displayController.getDisplayContext(taskInfo.displayId)
+
+ fun initIfNeeded() {
+ if (!isInitialised) {
+ initVeil()
+ isInitialised = true
+ }
+ }
+
+ private fun initVeil() {
+ val baseActivity = taskInfo.baseActivity
+ if (baseActivity == null) {
+ Slog.e(TAG, "Base activity component not found in task")
+ return
+ }
+ val resizeVeilIconFactory =
+ displayContext?.let {
+ createIconFactory(displayContext, R.dimen.desktop_mode_resize_veil_icon_size)
+ } ?: return
+ val pm = context.getApplicationContext().getPackageManager()
+ val activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */)
+ val provider = IconProvider(displayContext)
+ val appIconDrawable = provider.getIcon(activityInfo)
+ resizeVeilBitmap =
+ resizeVeilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT)
+ resizeVeil =
+ ResizeVeil(
+ context = displayContext,
+ displayController = displayController,
+ appIcon = resizeVeilBitmap,
+ parentSurface = desktopModeWindowDecoration.getLeash(),
+ surfaceControlTransactionSupplier = transactionSupplier,
+ taskInfo = taskInfo,
+ )
+ }
+
+ fun showVeil(t: Transaction) =
+ resizeVeil.updateTransactionWithShowVeil(
+ t,
+ desktopModeWindowDecoration.getLeash(),
+ bounds,
+ taskInfo,
+ )
+
+ fun updateVeil(t: Transaction) = resizeVeil.updateTransactionWithResizeVeil(t, newBounds)
+
+ fun hideVeil() = resizeVeil.hideVeil()
+
+ private fun createIconFactory(context: Context, dimensions: Int): BaseIconFactory {
+ val resources: Resources = context.resources
+ val densityDpi: Int = resources.getDisplayMetrics().densityDpi
+ val iconSize: Int = resources.getDimensionPixelSize(dimensions)
+ return BaseIconFactory(context, densityDpi, iconSize)
+ }
+
+ fun getLeash(): SurfaceControl = desktopModeWindowDecoration.getLeash()
+
+ fun dispose() {
+ if (isInitialised) resizeVeil.dispose()
+ }
+ }
+
+ private fun isTilingFocusRemoved(taskInfo: RunningTaskInfo): Boolean {
+ return taskInfo.isFocused &&
+ isTilingFocused &&
+ taskInfo.taskId != leftTaskResizingHelper?.taskInfo?.taskId &&
+ taskInfo.taskId != rightTaskResizingHelper?.taskInfo?.taskId
+ }
+
+ override fun onFocusTaskChanged(taskInfo: RunningTaskInfo?) {
+ if (taskInfo != null) {
+ moveTiledPairToFront(taskInfo)
+ }
+ }
+
+ private fun isTilingRefocused(taskInfo: RunningTaskInfo): Boolean {
+ return !isTilingFocused &&
+ taskInfo.isFocused &&
+ (taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId ||
+ taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId)
+ }
+
+ private fun buildTiledTasksMoveToFront(leftOnTop: Boolean): WindowContainerTransaction {
+ val wct = WindowContainerTransaction()
+ val leftTiledTask = leftTaskResizingHelper ?: return wct
+ val rightTiledTask = rightTaskResizingHelper ?: return wct
+ if (leftOnTop) {
+ wct.reorder(rightTiledTask.taskInfo.token, true)
+ wct.reorder(leftTiledTask.taskInfo.token, true)
+ } else {
+ wct.reorder(leftTiledTask.taskInfo.token, true)
+ wct.reorder(rightTiledTask.taskInfo.token, true)
+ }
+ return wct
+ }
+
+ fun removeTaskIfTiled(
+ taskId: Int,
+ taskVanished: Boolean = false,
+ shouldDelayUpdate: Boolean = false,
+ ) {
+ if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
+ removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate)
+ leftTaskResizingHelper = null
+ rightTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+ tearDownTiling()
+ return
+ }
+
+ if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+ removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate)
+ rightTaskResizingHelper = null
+ leftTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+ tearDownTiling()
+ }
+ }
+
+ fun onUserChange() {
+ if (leftTaskResizingHelper != null) {
+ removeTask(leftTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true)
+ leftTaskResizingHelper = null
+ }
+ if (rightTaskResizingHelper != null) {
+ removeTask(rightTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true)
+ rightTaskResizingHelper = null
+ }
+ tearDownTiling()
+ }
+
+ private fun removeTask(
+ appResizingHelper: AppResizingHelper?,
+ taskVanished: Boolean = false,
+ shouldDelayUpdate: Boolean,
+ ) {
+ if (appResizingHelper == null) return
+ if (!taskVanished) {
+ appResizingHelper.desktopModeWindowDecoration.removeDragResizeListener(this)
+ appResizingHelper.desktopModeWindowDecoration.updateDisabledResizingEdge(
+ NONE,
+ shouldDelayUpdate,
+ )
+ }
+ appResizingHelper.dispose()
+ }
+
+ fun onOverviewAnimationStateChange(isRunning: Boolean) {
+ if (!isTilingManagerInitialised) return
+
+ if (isRunning) {
+ desktopTilingDividerWindowManager?.hideDividerBar()
+ } else if (allTiledTasksVisible()) {
+ desktopTilingDividerWindowManager?.showDividerBar()
+ }
+ }
+
+ override fun onTaskVanished(taskInfo: RunningTaskInfo?) {
+ val taskId = taskInfo?.taskId ?: return
+ removeTaskIfTiled(taskId, taskVanished = true, shouldDelayUpdate = true)
+ }
+
+ fun moveTiledPairToFront(taskInfo: RunningTaskInfo): Boolean {
+ if (!isTilingManagerInitialised) return false
+
+ // If a task that isn't tiled is being focused, let the generic handler do the work.
+ if (isTilingFocusRemoved(taskInfo)) {
+ isTilingFocused = false
+ return false
+ }
+
+ val leftTiledTask = leftTaskResizingHelper ?: return false
+ val rightTiledTask = rightTaskResizingHelper ?: return false
+ if (!allTiledTasksVisible()) return false
+ val isLeftOnTop = taskInfo.taskId == leftTiledTask.taskInfo.taskId
+ if (isTilingRefocused(taskInfo)) {
+ val t = transactionSupplier.get()
+ isTilingFocused = true
+ if (taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
+ desktopTilingDividerWindowManager?.onRelativeLeashChanged(
+ leftTiledTask.getLeash(),
+ t,
+ )
+ }
+ if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+ desktopTilingDividerWindowManager?.onRelativeLeashChanged(
+ rightTiledTask.getLeash(),
+ t,
+ )
+ }
+ transitions.startTransition(
+ TRANSIT_TO_FRONT,
+ buildTiledTasksMoveToFront(isLeftOnTop),
+ null,
+ )
+ t.apply()
+ return true
+ }
+ return false
+ }
+
+ private fun allTiledTasksVisible(): Boolean {
+ val leftTiledTask = leftTaskResizingHelper ?: return false
+ val rightTiledTask = rightTaskResizingHelper ?: return false
+ return taskRepository.isVisibleTask(leftTiledTask.taskInfo.taskId) &&
+ taskRepository.isVisibleTask(rightTiledTask.taskInfo.taskId)
+ }
+
+ private fun isResizeWithinSizeConstraints(
+ newLeftBounds: Rect,
+ newRightBounds: Rect,
+ leftBounds: Rect,
+ rightBounds: Rect,
+ stableBounds: Rect,
+ ): Boolean {
+ return DragPositioningCallbackUtility.isExceedingWidthConstraint(
+ newLeftBounds.width(),
+ leftBounds.width(),
+ stableBounds,
+ displayController,
+ leftTaskResizingHelper?.desktopModeWindowDecoration,
+ ) ||
+ DragPositioningCallbackUtility.isExceedingWidthConstraint(
+ newRightBounds.width(),
+ rightBounds.width(),
+ stableBounds,
+ displayController,
+ rightTaskResizingHelper?.desktopModeWindowDecoration,
+ )
+ }
+
+ private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect {
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return Rect()
+
+ val stableBounds = Rect()
+ displayLayout.getStableBounds(stableBounds)
+ val leftTiledTask = leftTaskResizingHelper
+ val rightTiledTask = rightTaskResizingHelper
+ val destinationWidth = stableBounds.width() / 2
+ return when (position) {
+ SnapPosition.LEFT -> {
+ val rightBound =
+ if (rightTiledTask == null) {
+ stableBounds.left + destinationWidth -
+ context.resources.getDimensionPixelSize(
+ R.dimen.split_divider_bar_width
+ ) / 2
+ } else {
+ rightTiledTask.bounds.left -
+ context.resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+ }
+ Rect(stableBounds.left, stableBounds.top, rightBound, stableBounds.bottom)
+ }
+
+ SnapPosition.RIGHT -> {
+ val leftBound =
+ if (leftTiledTask == null) {
+ stableBounds.right - destinationWidth +
+ context.resources.getDimensionPixelSize(
+ R.dimen.split_divider_bar_width
+ ) / 2
+ } else {
+ leftTiledTask.bounds.right +
+ context.resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+ }
+ Rect(leftBound, stableBounds.top, stableBounds.right, stableBounds.bottom)
+ }
+ }
+ }
+
+ private fun inflateDividerBounds(displayLayout: DisplayLayout): Rect {
+ val stableBounds = Rect()
+ displayLayout.getStableBounds(stableBounds)
+
+ val leftDividerBounds = leftTaskResizingHelper?.bounds?.right ?: return Rect()
+ val rightDividerBounds = rightTaskResizingHelper?.bounds?.left ?: return Rect()
+
+ // Bounds should never be null here, so assertion is necessary otherwise it's illegal state.
+ return Rect(leftDividerBounds, stableBounds.top, rightDividerBounds, stableBounds.bottom)
+ }
+
+ private fun tearDownTiling() {
+ if (isTilingManagerInitialised) shellTaskOrganizer.removeFocusListener(this)
+
+ if (leftTaskResizingHelper == null && rightTaskResizingHelper == null) {
+ shellTaskOrganizer.removeTaskVanishedListener(this)
+ }
+ isTilingFocused = false
+ isTilingManagerInitialised = false
+ desktopTilingDividerWindowManager?.release()
+ desktopTilingDividerWindowManager = null
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.kt
new file mode 100644
index 0000000..b3b30ad
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+/** Divider move callback to whichever entity that handles the moving logic. */
+interface DividerMoveCallback {
+ /** Called on the divider move start gesture. */
+ fun onDividerMoveStart(pos: Int)
+
+ /** Called on the divider moved by dragging it. */
+ fun onDividerMove(pos: Int): Boolean
+
+ /** Called on divider move gesture end. */
+ fun onDividerMovedEnd(pos: Int)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
new file mode 100644
index 0000000..065a5d7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.provider.DeviceConfig
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.PointerIcon
+import android.view.View
+import android.view.ViewConfiguration
+import android.widget.FrameLayout
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.wm.shell.R
+import com.android.wm.shell.common.split.DividerHandleView
+import com.android.wm.shell.common.split.DividerRoundedCorner
+import com.android.wm.shell.shared.animation.Interpolators
+import com.android.wm.shell.windowdecor.DragDetector
+
+/** Divider for tiling split screen, currently mostly a copy of [DividerView]. */
+class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.MotionEventHandler {
+ private val paint = Paint()
+ private val backgroundRect = Rect()
+
+ private lateinit var callback: DividerMoveCallback
+ private lateinit var handle: DividerHandleView
+ private lateinit var corners: DividerRoundedCorner
+ private var touchElevation = 0
+
+ private var moving = false
+ private var startPos = 0
+ var handleRegionWidth: Int = 0
+ private var handleRegionHeight = 0
+ private var lastAcceptedPos = 0
+ @VisibleForTesting
+ var handleStartY = 0
+ @VisibleForTesting
+ var handleEndY = 0
+ private var canResize = false
+ /**
+ * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with
+ * insets.
+ */
+ private val dividerBounds = Rect()
+ private var dividerBar: FrameLayout? = null
+ private lateinit var dragDetector: DragDetector
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ ) : super(context, attrs, defStyleAttr)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ defStyleRes: Int,
+ ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ /** Sets up essential dependencies of the divider bar. */
+ fun setup(dividerMoveCallback: DividerMoveCallback, dividerBounds: Rect) {
+ callback = dividerMoveCallback
+ this.dividerBounds.set(dividerBounds)
+ handle.setIsLeftRightSplit(true)
+ corners.setIsLeftRightSplit(true)
+ handleRegionHeight =
+ resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_width)
+
+ handleRegionWidth =
+ resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_height)
+ initHandleYCoordinates()
+ dragDetector =
+ DragDetector(
+ this,
+ /* holdToDragMinDurationMs= */ 0,
+ ViewConfiguration.get(mContext).scaledTouchSlop,
+ )
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ dividerBar = requireViewById(R.id.divider_bar)
+ handle = requireViewById(R.id.docked_divider_handle)
+ corners = requireViewById(R.id.docked_divider_rounded_corner)
+ touchElevation =
+ resources.getDimensionPixelSize(R.dimen.docked_stack_divider_lift_elevation)
+ setOnTouchListener(this)
+ setWillNotDraw(false)
+ paint.color = resources.getColor(R.color.split_divider_background, null)
+ paint.isAntiAlias = true
+ paint.style = Paint.Style.FILL
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ if (changed) {
+ val dividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+ val backgroundLeft = (width - dividerSize) / 2
+ val backgroundTop = 0
+ val backgroundRight = left + dividerSize
+ val backgroundBottom = height
+ backgroundRect.set(backgroundLeft, backgroundTop, backgroundRight, backgroundBottom)
+ }
+ }
+
+ override fun onResolvePointerIcon(event: MotionEvent, pointerIndex: Int): PointerIcon =
+ PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW)
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean =
+ dragDetector.onMotionEvent(v, event)
+
+ private fun setTouching() {
+ handle.setTouching(true, true)
+ // Lift handle as well so it doesn't get behind the background, even though it doesn't
+ // cast shadow.
+ handle
+ .animate()
+ .setInterpolator(Interpolators.TOUCH_RESPONSE)
+ .setDuration(TOUCH_ANIMATION_DURATION)
+ .translationZ(touchElevation.toFloat())
+ .start()
+ }
+
+ private fun releaseTouching() {
+ handle.setTouching(false, true)
+ handle
+ .animate()
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+ .translationZ(0f)
+ .start()
+ }
+
+ override fun onHoverEvent(event: MotionEvent): Boolean {
+ if (
+ !DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED,
+ /* defaultValue = */ false,
+ )
+ ) {
+ return false
+ }
+
+ if (event.action == MotionEvent.ACTION_HOVER_ENTER) {
+ setHovering()
+ return true
+ }
+ if (event.action == MotionEvent.ACTION_HOVER_EXIT) {
+ releaseHovering()
+ return true
+ }
+ return false
+ }
+
+ @VisibleForTesting
+ fun setHovering() {
+ handle.setHovering(true, true)
+ handle
+ .animate()
+ .setInterpolator(Interpolators.TOUCH_RESPONSE)
+ .setDuration(TOUCH_ANIMATION_DURATION)
+ .translationZ(touchElevation.toFloat())
+ .start()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.drawRect(backgroundRect, paint)
+ }
+
+ @VisibleForTesting
+ fun releaseHovering() {
+ handle.setHovering(false, true)
+ handle
+ .animate()
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+ .translationZ(0f)
+ .start()
+ }
+
+ override fun handleMotionEvent(v: View?, event: MotionEvent): Boolean {
+ val touchPos = event.rawX.toInt()
+ val yTouchPosInDivider = event.y.toInt()
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ if (!isWithinHandleRegion(yTouchPosInDivider)) return true
+ callback.onDividerMoveStart(touchPos)
+ setTouching()
+ startPos = touchPos
+ canResize = true
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ if (!canResize) return true
+ if (!moving) {
+ startPos = touchPos
+ moving = true
+ }
+
+ val pos = dividerBounds.left + touchPos - startPos
+ if (callback.onDividerMove(pos)) {
+ lastAcceptedPos = touchPos
+ }
+ }
+
+ MotionEvent.ACTION_CANCEL,
+ MotionEvent.ACTION_UP -> {
+ if (!canResize) return true
+ dividerBounds.left = dividerBounds.left + lastAcceptedPos - startPos
+ if (moving) {
+ callback.onDividerMovedEnd(dividerBounds.left)
+ moving = false
+ canResize = false
+ }
+
+ releaseTouching()
+ }
+ }
+ return true
+ }
+
+ private fun isWithinHandleRegion(touchYPos: Int): Boolean {
+ return touchYPos in handleStartY..handleEndY
+ }
+
+ private fun initHandleYCoordinates() {
+ handleStartY = (dividerBounds.height() - handleRegionHeight) / 2
+ handleEndY = handleStartY + handleRegionHeight
+ }
+
+ companion object {
+ const val TOUCH_ANIMATION_DURATION: Long = 150
+ const val TOUCH_RELEASE_ANIMATION_DURATION: Long = 200
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index bc2b36c..0a6dfbf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -128,6 +128,8 @@
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.function.Consumer
@@ -223,6 +225,10 @@
@Mock lateinit var motionEvent: MotionEvent
private lateinit var mockitoSession: StaticMockitoSession
+ @Mock
+ private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
+ @Mock
+ private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration
private lateinit var controller: DesktopTasksController
private lateinit var shellInit: ShellInit
private lateinit var taskRepository: DesktopRepository
@@ -336,6 +342,7 @@
mockInputManager,
mockFocusTransitionObserver,
desktopModeEventLogger,
+ desktopTilingDecorViewModel,
)
}
@@ -2835,7 +2842,9 @@
Rect(100, -100, 500, 1000), /* currentDragBounds */
Rect(0, 50, 2000, 2000), /* validDragArea */
Rect() /* dragStartBounds */,
- motionEvent)
+ motionEvent,
+ desktopWindowDecoration,
+ )
val rectAfterEnd = Rect(100, 50, 500, 1150)
verify(transitions)
.startTransition(
@@ -2871,7 +2880,9 @@
currentDragBounds, /* currentDragBounds */
Rect(0, 50, 2000, 2000) /* validDragArea */,
Rect() /* dragStartBounds */,
- motionEvent)
+ motionEvent,
+ desktopWindowDecoration,
+ )
verify(transitions)
@@ -3135,6 +3146,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() {
val bounds = Rect(100, 100, 300, 300)
val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
@@ -3150,7 +3162,8 @@
STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
)
- controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
+ controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT,
+ ResizeTrigger.SNAP_LEFT_MENU, motionEvent, desktopWindowDecoration)
// Assert bounds set to stable bounds
val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
@@ -3165,6 +3178,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() {
// Set up task to already be in snapped-left bounds
val bounds = Rect(
@@ -3180,8 +3194,8 @@
// Attempt to snap left again
val currentDragBounds = Rect(bounds).apply { offset(-100, 0) }
- controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
-
+ controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT,
+ ResizeTrigger.SNAP_LEFT_MENU, motionEvent, desktopWindowDecoration)
// Assert that task is NOT updated via WCT
verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
@@ -3204,7 +3218,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING)
+ @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, Flags.FLAG_ENABLE_TILE_RESIZING)
fun handleSnapResizingTask_nonResizable_snapsToHalfScreen() {
val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply {
isResizeable = false
@@ -3215,7 +3229,9 @@
Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom)
controller.handleSnapResizingTask(
- task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent
+
+ task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent,
+ desktopWindowDecoration
)
val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
assertThat(findBoundsChange(wct, task)).isEqualTo(
@@ -3239,7 +3255,8 @@
val currentDragBounds = Rect(0, 100, 300, 500)
controller.handleSnapResizingTask(
- task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent)
+ task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent,
+ desktopWindowDecoration)
verify(mReturnToDragStartAnimator).start(
eq(task.taskId),
eq(mockSurface),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index f95b0d1..8dd1545 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -31,6 +31,7 @@
import android.app.ActivityManager;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.SurfaceControl;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -49,6 +50,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -65,6 +67,9 @@
@RunWith(AndroidJUnit4.class)
public final class FreeformTaskListenerTests extends ShellTestCase {
+ @Rule
+ public final SetFlagsRule setFlagsRule = new SetFlagsRule();
+
@Mock
private ShellTaskOrganizer mTaskOrganizer;
@Mock
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index c5526fc..c42be7f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -666,7 +666,8 @@
eq(currentBounds),
eq(SnapPosition.LEFT),
eq(ResizeTrigger.SNAP_LEFT_MENU),
- eq(null)
+ eq(null),
+ eq(decor)
)
assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface)
}
@@ -706,7 +707,8 @@
eq(currentBounds),
eq(SnapPosition.LEFT),
eq(ResizeTrigger.SNAP_LEFT_MENU),
- eq(null)
+ eq(null),
+ eq(decor),
)
assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
}
@@ -727,7 +729,9 @@
verify(mockDesktopTasksController, never())
.snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
eq(ResizeTrigger.MAXIMIZE_BUTTON),
- eq(null))
+ eq(null),
+ eq(decor),
+ )
verify(mockToast).show()
}
@@ -750,7 +754,8 @@
eq(currentBounds),
eq(SnapPosition.RIGHT),
eq(ResizeTrigger.SNAP_RIGHT_MENU),
- eq(null)
+ eq(null),
+ eq(decor),
)
assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
}
@@ -790,7 +795,8 @@
eq(currentBounds),
eq(SnapPosition.RIGHT),
eq(ResizeTrigger.SNAP_RIGHT_MENU),
- eq(null)
+ eq(null),
+ eq(decor),
)
assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
}
@@ -811,7 +817,9 @@
verify(mockDesktopTasksController, never())
.snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
eq(ResizeTrigger.MAXIMIZE_BUTTON),
- eq(null))
+ eq(null),
+ eq(decor),
+ )
verify(mockToast).show()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
index 57469bf..e7d328e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -65,7 +65,7 @@
private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10;
private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry(
TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET,
- FINE_CORNER_SIZE, LARGE_CORNER_SIZE);
+ FINE_CORNER_SIZE, LARGE_CORNER_SIZE, DragResizeWindowGeometry.DisabledEdge.NONE);
// Points in the edge resize handle. Note that coordinates start from the top left.
private static final Point TOP_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2,
-EDGE_RESIZE_THICKNESS / 2);
@@ -100,23 +100,25 @@
GEOMETRY,
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET, FINE_CORNER_SIZE,
- LARGE_CORNER_SIZE))
+ LARGE_CORNER_SIZE, DragResizeWindowGeometry.DisabledEdge.NONE))
.addEqualityGroup(
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
- FINE_CORNER_SIZE, LARGE_CORNER_SIZE),
+ FINE_CORNER_SIZE, LARGE_CORNER_SIZE,
+ DragResizeWindowGeometry.DisabledEdge.NONE),
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
- FINE_CORNER_SIZE, LARGE_CORNER_SIZE))
+ FINE_CORNER_SIZE, LARGE_CORNER_SIZE,
+ DragResizeWindowGeometry.DisabledEdge.NONE))
.addEqualityGroup(
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
FINE_CORNER_SIZE,
- LARGE_CORNER_SIZE + 5),
+ LARGE_CORNER_SIZE + 5, DragResizeWindowGeometry.DisabledEdge.NONE),
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
FINE_CORNER_SIZE,
- LARGE_CORNER_SIZE + 5))
+ LARGE_CORNER_SIZE + 5, DragResizeWindowGeometry.DisabledEdge.NONE))
.testEquals();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index ca1f9ab..3b80cb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -47,6 +47,7 @@
import java.util.function.Supplier
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
+import org.mockito.kotlin.times
import org.mockito.Mockito.`when` as whenever
/**
@@ -66,7 +67,7 @@
@Mock
private lateinit var mockWindowDecoration: WindowDecoration<*>
@Mock
- private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
+ private lateinit var mockDragEventListener: DragPositioningCallbackUtility.DragEventListener
@Mock
private lateinit var taskToken: WindowContainerToken
@@ -140,7 +141,7 @@
mockTransitions,
mockWindowDecoration,
mockDisplayController,
- mockDragStartListener,
+ mockDragEventListener,
mockTransactionFactory
)
}
@@ -220,6 +221,7 @@
change.configuration.windowConfiguration.bounds == rectAfterMove
}
})
+ verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID))
taskPositioner.onDragPositioningEnd(
STARTING_BOUNDS.left.toFloat() + 10,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 1dfbd67..e7df864 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -79,7 +79,7 @@
@Mock
private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration
@Mock
- private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
+ private lateinit var mockDragEventListener: DragPositioningCallbackUtility.DragEventListener
@Mock
private lateinit var taskToken: WindowContainerToken
@@ -156,7 +156,7 @@
mockShellTaskOrganizer,
mockDesktopWindowDecoration,
mockDisplayController,
- mockDragStartListener,
+ mockDragEventListener,
mockTransactionFactory,
mockTransitions,
mockInteractionJankMonitor,
@@ -433,6 +433,7 @@
// isResizingOrAnimating should be set to true after move during a resize
Assert.assertTrue(taskPositioner.isResizingOrAnimating)
+ verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID))
taskPositioner.onDragPositioningEnd(
STARTING_BOUNDS.left.toFloat(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
new file mode 100644
index 0000000..0ccd424
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+import android.content.Context
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingDecorViewModelTest : ShellTestCase() {
+ private val contextMock: Context = mock()
+ private val displayControllerMock: DisplayController = mock()
+ private val rootTdaOrganizerMock: RootTaskDisplayAreaOrganizer = mock()
+ private val syncQueueMock: SyncTransactionQueue = mock()
+ private val transitionsMock: Transitions = mock()
+ private val shellTaskOrganizerMock: ShellTaskOrganizer = mock()
+ private val desktopRepository: DesktopRepository = mock()
+ private val toggleResizeDesktopTaskTransitionHandlerMock:
+ ToggleResizeDesktopTaskTransitionHandler =
+ mock()
+ private val returnToDragStartAnimatorMock: ReturnToDragStartAnimator = mock()
+
+ private val desktopModeWindowDecorationMock: DesktopModeWindowDecoration = mock()
+ private val desktopTilingDecoration: DesktopTilingWindowDecoration = mock()
+ private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
+
+ @Before
+ fun setUp() {
+ desktopTilingDecorViewModel =
+ DesktopTilingDecorViewModel(
+ contextMock,
+ displayControllerMock,
+ rootTdaOrganizerMock,
+ syncQueueMock,
+ transitionsMock,
+ shellTaskOrganizerMock,
+ toggleResizeDesktopTaskTransitionHandlerMock,
+ returnToDragStartAnimatorMock,
+ desktopRepository,
+ )
+ }
+
+ @Test
+ fun testTiling_shouldCreate_newTilingDecoration() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ task1.displayId = 1
+ task2.displayId = 2
+
+ desktopTilingDecorViewModel.snapToHalfScreen(
+ task1,
+ desktopModeWindowDecorationMock,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ assertThat(desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.size())
+ .isEqualTo(1)
+ desktopTilingDecorViewModel.snapToHalfScreen(
+ task2,
+ desktopModeWindowDecorationMock,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ assertThat(desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.size())
+ .isEqualTo(2)
+ }
+
+ @Test
+ fun removeTile_shouldCreate_newTilingDecoration() {
+ val task1 = createFreeformTask()
+ task1.displayId = 1
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 1,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.removeTaskIfTiled(task1.displayId, task1.taskId)
+
+ verify(desktopTilingDecoration, times(1)).removeTaskIfTiled(any(), any(), any())
+ }
+
+ @Test
+ fun moveTaskToFront_shouldRoute_toCorrectTilingDecoration() {
+
+ val task1 = createFreeformTask()
+ task1.displayId = 1
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 1,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.moveTaskToFrontIfTiled(task1)
+
+ verify(desktopTilingDecoration, times(1)).moveTiledPairToFront(any())
+ }
+
+ @Test
+ fun overviewAnimation_starting_ShouldNotifyAllDecorations() {
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 1,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 2,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.onOverviewAnimationStateChange(true)
+
+ verify(desktopTilingDecoration, times(2)).onOverviewAnimationStateChange(any())
+ }
+
+ @Test
+ fun userChange_starting_allTilingSessionsShouldBeDestroyed() {
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 1,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 2,
+ desktopTilingDecoration,
+ )
+
+ desktopTilingDecorViewModel.onUserChange()
+
+ verify(desktopTilingDecoration, times(2)).onUserChange()
+ }
+
+ companion object {
+ private val BOUNDS = Rect(1, 2, 3, 4)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
new file mode 100644
index 0000000..0ee3f46
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.SyncTransactionQueue
+import java.util.function.Supplier
+import kotlin.test.Test
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingDividerWindowManagerTest : ShellTestCase() {
+ private lateinit var config: Configuration
+
+ private var windowName: String = "Tiling"
+
+ private val leashMock = mock<SurfaceControl>()
+
+ private val syncQueueMock = mock<SyncTransactionQueue>()
+
+ private val transitionHandlerMock = mock<DesktopTilingWindowDecoration>()
+
+ private val transactionSupplierMock = mock<Supplier<SurfaceControl.Transaction>>()
+
+ private val surfaceControl = mock<SurfaceControl>()
+
+ private val transaction = mock<SurfaceControl.Transaction>()
+
+ private lateinit var desktopTilingWindowManager: DesktopTilingDividerWindowManager
+
+ @Before
+ fun setup() {
+ config = Configuration()
+ config.setToDefaults()
+ desktopTilingWindowManager =
+ DesktopTilingDividerWindowManager(
+ config,
+ windowName,
+ mContext,
+ leashMock,
+ syncQueueMock,
+ transitionHandlerMock,
+ transactionSupplierMock,
+ BOUNDS,
+ )
+ }
+
+ @Test
+ @UiThreadTest
+ fun testWindowManager_isInitialisedAndReleased() {
+ whenever(transactionSupplierMock.get()).thenReturn(transaction)
+ whenever(transaction.hide(any())).thenReturn(transaction)
+ whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction)
+ whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction)
+ whenever(transaction.remove(any())).thenReturn(transaction)
+
+ desktopTilingWindowManager.generateViewHost(surfaceControl)
+
+ // Ensure a surfaceControl transaction runs to show the divider.
+ verify(transactionSupplierMock, times(1)).get()
+ verify(syncQueueMock, times(1)).runInSync(any())
+
+ desktopTilingWindowManager.release()
+ verify(transaction, times(1)).hide(any())
+ verify(transaction, times(1)).remove(any())
+ verify(transaction, times(1)).apply()
+ }
+
+ companion object {
+ private val BOUNDS = Rect(1, 2, 3, 4)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
new file mode 100644
index 0000000..0b04a21
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.capture
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingWindowDecorationTest : ShellTestCase() {
+
+ private val context: Context = mock()
+
+ private val syncQueue: SyncTransactionQueue = mock()
+
+ private val displayController: DisplayController = mock()
+ private val displayId: Int = 0
+
+ private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer = mock()
+
+ private val transitions: Transitions = mock()
+
+ private val shellTaskOrganizer: ShellTaskOrganizer = mock()
+
+ private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler =
+ mock()
+
+ private val returnToDragStartAnimator: ReturnToDragStartAnimator = mock()
+
+ private val desktopWindowDecoration: DesktopModeWindowDecoration = mock()
+
+ private val displayLayout: DisplayLayout = mock()
+
+ private val resources: Resources = mock()
+ private val surfaceControlMock: SurfaceControl = mock()
+ private val transaction: SurfaceControl.Transaction = mock()
+ private val tiledTaskHelper: DesktopTilingWindowDecoration.AppResizingHelper = mock()
+ private val transition: IBinder = mock()
+ private val info: TransitionInfo = mock()
+ private val finishCallback: Transitions.TransitionFinishCallback = mock()
+ private val desktopRepository: DesktopRepository = mock()
+ private val desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager = mock()
+ private lateinit var tilingDecoration: DesktopTilingWindowDecoration
+
+ private val split_divider_width = 10
+
+ @Captor private lateinit var wctCaptor: ArgumentCaptor<WindowContainerTransaction>
+
+ @Before
+ fun setUp() {
+ tilingDecoration =
+ DesktopTilingWindowDecoration(
+ context,
+ syncQueue,
+ displayController,
+ displayId,
+ rootTdaOrganizer,
+ transitions,
+ shellTaskOrganizer,
+ toggleResizeDesktopTaskTransitionHandler,
+ returnToDragStartAnimator,
+ desktopRepository,
+ )
+ }
+
+ @Test
+ fun taskTiled_toCorrectBounds_leftTile() {
+ val task1 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+
+ verify(toggleResizeDesktopTaskTransitionHandler).startTransition(capture(wctCaptor), any())
+ for (change in wctCaptor.value.changes) {
+ val bounds = change.value.configuration.windowConfiguration.bounds
+ val leftBounds = getLeftTaskBounds()
+ assertRectEqual(bounds, leftBounds)
+ }
+ }
+
+ @Test
+ fun taskTiled_toCorrectBounds_rightTile() {
+ // Setup
+ val task1 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+
+ verify(toggleResizeDesktopTaskTransitionHandler).startTransition(capture(wctCaptor), any())
+ for (change in wctCaptor.value.changes) {
+ val bounds = change.value.configuration.windowConfiguration.bounds
+ val leftBounds = getRightTaskBounds()
+ assertRectEqual(bounds, leftBounds)
+ }
+ }
+
+ @Test
+ fun taskTiled_notAnimated_whenTilingPositionNotChange() {
+ val task1 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(desktopWindowDecoration.getLeash()).thenReturn(surfaceControlMock)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ task1.configuration.windowConfiguration.setBounds(getLeftTaskBounds())
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ NON_STABLE_BOUNDS_MOCK,
+ )
+
+ verify(toggleResizeDesktopTaskTransitionHandler, times(1))
+ .startTransition(capture(wctCaptor), any())
+ verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), any())
+ for (change in wctCaptor.value.changes) {
+ val bounds = change.value.configuration.windowConfiguration.bounds
+ val leftBounds = getLeftTaskBounds()
+ assertRectEqual(bounds, leftBounds)
+ }
+ }
+
+ @Test
+ fun taskNotTiled_notBroughtToFront_tilingNotInitialised() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+
+ assertThat(tilingDecoration.moveTiledPairToFront(task2)).isFalse()
+ verify(transitions, never()).startTransition(any(), any(), any())
+ }
+
+ @Test
+ fun taskNotTiled_notBroughtToFront_taskNotTiled() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val task3 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+
+ assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse()
+ verify(transitions, never()).startTransition(any(), any(), any())
+ }
+
+ @Test
+ fun taskTiled_broughtToFront_alreadyInFrontNoAction() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ task1.isFocused = true
+
+ assertThat(tilingDecoration.moveTiledPairToFront(task1)).isFalse()
+ verify(transitions, never()).startTransition(any(), any(), any())
+ }
+
+ @Test
+ fun taskTiled_broughtToFront_bringToFront() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val task3 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(desktopWindowDecoration.getLeash()).thenReturn(surfaceControlMock)
+ whenever(desktopRepository.isVisibleTask(any())).thenReturn(true)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ task1.isFocused = true
+ task3.isFocused = true
+
+ assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse()
+ assertThat(tilingDecoration.moveTiledPairToFront(task1)).isTrue()
+ verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null))
+ }
+
+ @Test
+ fun taskTiledTasks_NotResized_BeforeTouchEndArrival() {
+ // Setup
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ desktopWindowDecoration.mTaskInfo = task1
+ task1.minWidth = 0
+ task1.minHeight = 0
+ initTiledTaskHelperMock(task1)
+ desktopWindowDecoration.mDecorWindowContext = context
+ whenever(resources.getBoolean(any())).thenReturn(true)
+
+ // Act
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+ tilingDecoration.rightTaskResizingHelper = tiledTaskHelper
+ tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+
+ // Assert
+ verify(transaction, times(1)).apply()
+ // Show should be called twice for each tiled app, to show the veil and the icon for each
+ // of them.
+ verify(tiledTaskHelper, times(2)).showVeil(any())
+
+ // Move again
+ tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+ verify(tiledTaskHelper, times(2)).updateVeil(any())
+ verify(transitions, never()).startTransition(any(), any(), any())
+
+ // End moving, no startTransition because bounds did not change.
+ tiledTaskHelper.newBounds.set(BOUNDS)
+ tilingDecoration.onDividerHandleDragEnd(BOUNDS, transaction)
+ verify(tiledTaskHelper, times(2)).hideVeil()
+ verify(transitions, never()).startTransition(any(), any(), any())
+
+ // Move then end again with bounds changing to ensure startTransition is called.
+ tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+ tilingDecoration.onDividerHandleDragEnd(BOUNDS, transaction)
+ verify(transitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), any(), eq(tilingDecoration))
+ // No hide veil until start animation is called.
+ verify(tiledTaskHelper, times(2)).hideVeil()
+
+ tilingDecoration.startAnimation(transition, info, transaction, transaction, finishCallback)
+ // the startAnimation function should hide the veils.
+ verify(tiledTaskHelper, times(4)).hideVeil()
+ }
+
+ @Test
+ fun taskTiled_shouldBeRemoved_whenTileBroken() {
+ val task1 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+
+ tilingDecoration.removeTaskIfTiled(task1.taskId)
+
+ assertThat(tilingDecoration.leftTaskResizingHelper).isNull()
+ verify(desktopWindowDecoration, times(1)).removeDragResizeListener(any())
+ verify(desktopWindowDecoration, times(1))
+ .updateDisabledResizingEdge(eq(DragResizeWindowGeometry.DisabledEdge.NONE), eq(false))
+ verify(tiledTaskHelper, times(1)).dispose()
+ }
+
+ @Test
+ fun taskNotTiled_shouldNotBeRemoved_whenNotTiled() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+
+ tilingDecoration.removeTaskIfTiled(task2.taskId)
+
+ assertThat(tilingDecoration.leftTaskResizingHelper).isNotNull()
+ verify(desktopWindowDecoration, never()).removeDragResizeListener(any())
+ verify(desktopWindowDecoration, never()).updateDisabledResizingEdge(any(), any())
+ verify(tiledTaskHelper, never()).dispose()
+ }
+
+ @Test
+ fun tasksTiled_shouldBeRemoved_whenSessionDestroyed() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+ tilingDecoration.rightTaskResizingHelper = tiledTaskHelper
+ tilingDecoration.desktopTilingDividerWindowManager = desktopTilingDividerWindowManager
+
+ tilingDecoration.onUserChange()
+
+ assertThat(tilingDecoration.leftTaskResizingHelper).isNull()
+ assertThat(tilingDecoration.rightTaskResizingHelper).isNull()
+ verify(desktopWindowDecoration, times(2)).removeDragResizeListener(any())
+ verify(tiledTaskHelper, times(2)).dispose()
+ }
+
+ private fun initTiledTaskHelperMock(taskInfo: ActivityManager.RunningTaskInfo) {
+ whenever(tiledTaskHelper.bounds).thenReturn(BOUNDS)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(taskInfo)
+ whenever(tiledTaskHelper.newBounds).thenReturn(Rect(BOUNDS))
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ }
+
+ private fun assertRectEqual(rect1: Rect, rect2: Rect) {
+ assertThat(rect1.left).isEqualTo(rect2.left)
+ assertThat(rect1.right).isEqualTo(rect2.right)
+ assertThat(rect1.top).isEqualTo(rect2.top)
+ assertThat(rect1.bottom).isEqualTo(rect2.bottom)
+ return
+ }
+
+ private fun getRightTaskBounds(): Rect {
+ val stableBounds = STABLE_BOUNDS_MOCK
+ val destinationWidth = stableBounds.width() / 2
+ val leftBound = stableBounds.right - destinationWidth + split_divider_width / 2
+ return Rect(leftBound, stableBounds.top, stableBounds.right, stableBounds.bottom)
+ }
+
+ private fun getLeftTaskBounds(): Rect {
+ val stableBounds = STABLE_BOUNDS_MOCK
+ val destinationWidth = stableBounds.width() / 2
+ val rightBound = stableBounds.left + destinationWidth - split_divider_width / 2
+ return Rect(stableBounds.left, stableBounds.top, rightBound, stableBounds.bottom)
+ }
+
+ companion object {
+ private val NON_STABLE_BOUNDS_MOCK = Rect(50, 55, 100, 100)
+ private val STABLE_BOUNDS_MOCK = Rect(0, 0, 100, 100)
+ private val BOUNDS = Rect(1, 2, 3, 4)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
new file mode 100644
index 0000000..fd5eb88
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.graphics.Rect
+import android.os.SystemClock
+import android.testing.AndroidTestingRunner
+import android.view.InputDevice
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TilingDividerViewTest : ShellTestCase() {
+
+ private lateinit var tilingDividerView: TilingDividerView
+
+ private val dividerMoveCallbackMock = mock<DividerMoveCallback>()
+
+ private val viewMock = mock<View>()
+
+ @Before
+ @UiThreadTest
+ fun setUp() {
+ tilingDividerView =
+ LayoutInflater.from(mContext).inflate(R.layout.tiling_split_divider, /* root= */ null)
+ as TilingDividerView
+ tilingDividerView.setup(dividerMoveCallbackMock, BOUNDS)
+ tilingDividerView.handleStartY = 0
+ tilingDividerView.handleEndY = 1500
+ }
+
+ @Test
+ @UiThreadTest
+ fun testCallbackOnTouch() {
+ val x = 5
+ val y = 5
+ val downTime: Long = SystemClock.uptimeMillis()
+
+ val downMotionEvent =
+ getMotionEvent(downTime, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat())
+ tilingDividerView.handleMotionEvent(viewMock, downMotionEvent)
+ verify(dividerMoveCallbackMock, times(1)).onDividerMoveStart(any())
+
+ val motionEvent =
+ getMotionEvent(downTime, MotionEvent.ACTION_MOVE, x.toFloat(), y.toFloat())
+ tilingDividerView.handleMotionEvent(viewMock, motionEvent)
+ verify(dividerMoveCallbackMock, times(1)).onDividerMove(any())
+
+ val upMotionEvent =
+ getMotionEvent(downTime, MotionEvent.ACTION_UP, x.toFloat(), y.toFloat())
+ tilingDividerView.handleMotionEvent(viewMock, upMotionEvent)
+ verify(dividerMoveCallbackMock, times(1)).onDividerMovedEnd(any())
+ }
+
+ private fun getMotionEvent(eventTime: Long, action: Int, x: Float, y: Float): MotionEvent {
+ val properties = MotionEvent.PointerProperties()
+ properties.id = 0
+ properties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN
+
+ val coords = MotionEvent.PointerCoords()
+ coords.pressure = 1f
+ coords.size = 1f
+ coords.x = x
+ coords.y = y
+
+ return MotionEvent.obtain(
+ eventTime,
+ eventTime,
+ action,
+ 1,
+ arrayOf(properties),
+ arrayOf(coords),
+ 0,
+ 0,
+ 1.0f,
+ 1.0f,
+ 0,
+ 0,
+ InputDevice.SOURCE_TOUCHSCREEN,
+ 0,
+ )
+ }
+
+ companion object {
+ private val BOUNDS = Rect(0, 0, 1500, 1500)
+ }
+}
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 2c8e352..57f5f52c 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -316,7 +316,7 @@
@NonNull
public Builder setMediaProjection(@NonNull MediaProjection projection) {
if (projection == null) {
- throw new IllegalArgumentException("Invalid null volume callback");
+ throw new IllegalArgumentException("Invalid null media projection");
}
mProjection = projection;
return this;
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 5a8763f..81a2e6a 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -159,3 +159,10 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "hearing_devices_ambient_volume_control"
+ namespace: "accessibility"
+ description: "Enable the ambient volume control in device details and hearing devices dialog."
+ bug: "357878944"
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3650f68..d124c02 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -378,6 +378,17 @@
}
flag {
+ name: "status_bar_show_audio_only_projection_chip"
+ namespace: "systemui"
+ description: "Show chip on the left side of the status bar when a user is only sharing *audio* "
+ "during a media projection"
+ bug: "373308507"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "status_bar_use_repos_for_call_chip"
namespace: "systemui"
description: "Use repositories as the source of truth for call notifications shown as a chip in"
@@ -729,6 +740,13 @@
}
flag {
+ name: "smartspace_swipe_event_logging"
+ namespace: "systemui"
+ description: "Log card swipe events in smartspace"
+ bug: "374150422"
+}
+
+flag {
name: "pin_input_field_styled_focus_state"
namespace: "systemui"
description: "Enables styled focus states on pin input field if keyboard is connected"
@@ -991,6 +1009,16 @@
}
flag {
+ name: "shortcut_helper_key_glyph"
+ namespace: "systemui"
+ description: "Allow showing key glyph in shortcut helper"
+ bug: "353902478"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "dream_overlay_bouncer_swipe_direction_filtering"
namespace: "systemui"
description: "do not initiate bouncer swipe when the direction is opposite of the expansion"
@@ -1631,4 +1659,11 @@
metadata {
purpose: PURPOSE_BUGFIX
}
-}
\ No newline at end of file
+}
+
+flag {
+ name: "shade_expands_on_status_bar_long_press"
+ namespace: "systemui"
+ description: "Expands the shade on long press of any status bar"
+ bug: "371224114"
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 476cced..5e1ac1f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -181,9 +181,11 @@
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.ui.viewmodel.ResizeInfo
+import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
import com.android.systemui.communal.util.DensityUtils.Companion.adjustedDp
import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView
import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import kotlin.math.max
@@ -665,6 +667,7 @@
maxHeightPx: Int,
modifier: Modifier = Modifier,
alpha: () -> Float = { 1f },
+ viewModel: ResizeableItemFrameViewModel,
onResize: (info: ResizeInfo) -> Unit = {},
content: @Composable (modifier: Modifier) -> Unit,
) {
@@ -680,6 +683,7 @@
enabled = enabled,
alpha = alpha,
modifier = modifier,
+ viewModel = viewModel,
onResize = onResize,
minHeightPx = minHeightPx,
maxHeightPx = maxHeightPx,
@@ -711,7 +715,7 @@
WidgetSizeInfo(minHeightPx, maxHeightPx)
}
} else {
- WidgetSizeInfo(0, Int.MAX_VALUE)
+ WidgetSizeInfo(0, 0)
}
}
@@ -796,6 +800,14 @@
false
}
+ val resizeableItemFrameViewModel =
+ rememberViewModel(
+ key = item.size.span,
+ traceName = "ResizeableItemFrame.viewModel.$index",
+ ) {
+ ResizeableItemFrameViewModel()
+ }
+
if (viewModel.isEditMode && dragDropState != null) {
val isItemDragging = dragDropState.draggingItemKey == item.key
val outlineAlpha by
@@ -821,6 +833,7 @@
)
}
.thenIf(isItemDragging) { Modifier.zIndex(1f) },
+ viewModel = resizeableItemFrameViewModel,
onResize = { resizeInfo -> contentListState.resize(index, resizeInfo) },
minHeightPx = widgetSizeInfo.minHeightPx,
maxHeightPx = widgetSizeInfo.maxHeightPx,
@@ -843,6 +856,7 @@
contentListState = contentListState,
interactionHandler = interactionHandler,
widgetSection = widgetSection,
+ resizeableItemFrameViewModel = resizeableItemFrameViewModel,
)
}
}
@@ -857,6 +871,7 @@
contentListState = contentListState,
interactionHandler = interactionHandler,
widgetSection = widgetSection,
+ resizeableItemFrameViewModel = resizeableItemFrameViewModel,
)
}
}
@@ -1080,6 +1095,7 @@
contentListState: ContentListState,
interactionHandler: RemoteViews.InteractionHandler?,
widgetSection: CommunalAppWidgetSection,
+ resizeableItemFrameViewModel: ResizeableItemFrameViewModel,
) {
when (model) {
is CommunalContentModel.WidgetContent.Widget ->
@@ -1093,6 +1109,7 @@
index,
contentListState,
widgetSection,
+ resizeableItemFrameViewModel,
)
is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
is CommunalContentModel.WidgetContent.DisabledWidget ->
@@ -1223,7 +1240,9 @@
index: Int,
contentListState: ContentListState,
widgetSection: CommunalAppWidgetSection,
+ resizeableItemFrameViewModel: ResizeableItemFrameViewModel,
) {
+ val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val accessibilityLabel =
remember(model, context) {
@@ -1234,6 +1253,10 @@
val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget)
val unselectWidgetActionLabel =
stringResource(R.string.accessibility_action_label_unselect_widget)
+
+ val shrinkWidgetLabel = stringResource(R.string.accessibility_action_label_shrink_widget)
+ val expandWidgetLabel = stringResource(R.string.accessibility_action_label_expand_widget)
+
val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle()
val selectedIndex =
selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
@@ -1292,6 +1315,29 @@
true
}
val actions = mutableListOf(deleteAction)
+
+ if (communalWidgetResizing() && resizeableItemFrameViewModel.canShrink()) {
+ actions.add(
+ CustomAccessibilityAction(shrinkWidgetLabel) {
+ coroutineScope.launch {
+ resizeableItemFrameViewModel.shrinkToNextAnchor()
+ }
+ true
+ }
+ )
+ }
+
+ if (communalWidgetResizing() && resizeableItemFrameViewModel.canExpand()) {
+ actions.add(
+ CustomAccessibilityAction(expandWidgetLabel) {
+ coroutineScope.launch {
+ resizeableItemFrameViewModel.expandToNextAnchor()
+ }
+ true
+ }
+ )
+ }
+
if (selectedIndex != null && selectedIndex != index) {
actions.add(
CustomAccessibilityAction(placeWidgetActionLabel) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
index 521330f..8e85432 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
@@ -56,7 +56,6 @@
import com.android.systemui.communal.ui.viewmodel.DragHandle
import com.android.systemui.communal.ui.viewmodel.ResizeInfo
import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
-import com.android.systemui.lifecycle.rememberViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@@ -192,16 +191,12 @@
maxHeightPx: Int = Int.MAX_VALUE,
resizeMultiple: Int = 1,
alpha: () -> Float = { 1f },
+ viewModel: ResizeableItemFrameViewModel,
onResize: (info: ResizeInfo) -> Unit = {},
content: @Composable () -> Unit,
) {
val brush = SolidColor(outlineColor)
val onResizeUpdated by rememberUpdatedState(onResize)
- val viewModel =
- rememberViewModel(key = currentSpan, traceName = "ResizeableItemFrame.viewModel") {
- ResizeableItemFrameViewModel()
- }
-
val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
val isDragging by
remember(viewModel) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
index 58c3fec..bd33e52 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
@@ -20,7 +20,9 @@
import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -29,11 +31,12 @@
import android.graphics.Rect;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.VelocityTracker;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
@@ -42,9 +45,12 @@
import com.android.systemui.ambient.touch.scrim.ScrimController;
import com.android.systemui.ambient.touch.scrim.ScrimManager;
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
+import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -58,10 +64,14 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.List;
import java.util.Optional;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@DisableFlags(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN)
public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase {
@@ -114,6 +124,11 @@
@Mock
KeyguardInteractor mKeyguardInteractor;
+ @Mock
+ WindowRootView mWindowRootView;
+
+ private SceneInteractor mSceneInteractor;
+
private static final float TOUCH_REGION = .3f;
private static final float MIN_BOUNCER_HEIGHT = .05f;
@@ -124,9 +139,21 @@
/* flags= */ 0
);
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
+ }
+
+ public BouncerFullscreenSwipeTouchHandlerTest(FlagsParameterization flags) {
+ super();
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
@Before
public void setup() {
mKosmos = new KosmosJavaAdapter(this);
+ mSceneInteractor = spy(mKosmos.getSceneInteractor());
+
MockitoAnnotations.initMocks(this);
mTouchHandler = new BouncerSwipeTouchHandler(
mKosmos.getTestScope(),
@@ -142,7 +169,9 @@
MIN_BOUNCER_HEIGHT,
mUiEventLogger,
mActivityStarter,
- mKeyguardInteractor);
+ mKeyguardInteractor,
+ mSceneInteractor,
+ Optional.of(() -> mWindowRootView));
when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -153,6 +182,38 @@
}
/**
+ * Makes sure that touches go to the scene container when the flag is on.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+ public void testSwipeUp_sendsTouchesToWindowRootView() {
+ mTouchHandler.onGlanceableTouchAvailable(true);
+ mTouchHandler.onSessionStart(mTouchSession);
+ ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+ verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+ final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
+
+ final int screenHeight = 100;
+ final float distanceY = screenHeight * 0.42f;
+
+ final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+ 0, screenHeight, 0);
+ final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+ 0, screenHeight - distanceY, 0);
+
+ assertThat(gestureListener.onScroll(event1, event2, 0,
+ distanceY))
+ .isTrue();
+
+ // Ensure only called once
+ verify(mSceneInteractor).onRemoteUserInputStarted(any());
+ verify(mWindowRootView).dispatchTouchEvent(event1);
+ verify(mWindowRootView).dispatchTouchEvent(event2);
+ }
+
+ /**
* Ensures expansion does not happen for full vertical swipes when touch is not available.
*/
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 9568167..494e0b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -26,6 +26,7 @@
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -37,12 +38,12 @@
import android.graphics.Region;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.VelocityTracker;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
@@ -52,9 +53,12 @@
import com.android.systemui.ambient.touch.scrim.ScrimManager;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
+import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -70,10 +74,14 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.List;
import java.util.Optional;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
private KosmosJavaAdapter mKosmos;
@@ -122,6 +130,9 @@
Region mRegion;
@Mock
+ WindowRootView mWindowRootView;
+
+ @Mock
CommunalViewModel mCommunalViewModel;
@Mock
@@ -130,6 +141,8 @@
@Captor
ArgumentCaptor<Rect> mRectCaptor;
+ private SceneInteractor mSceneInteractor;
+
private static final float TOUCH_REGION = .3f;
private static final int SCREEN_WIDTH_PX = 1024;
private static final int SCREEN_HEIGHT_PX = 100;
@@ -142,9 +155,21 @@
/* flags= */ 0
);
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
+ }
+
+ public BouncerSwipeTouchHandlerTest(FlagsParameterization flags) {
+ super();
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
@Before
public void setup() {
mKosmos = new KosmosJavaAdapter(this);
+ mSceneInteractor = spy(mKosmos.getSceneInteractor());
+
MockitoAnnotations.initMocks(this);
mTouchHandler = new BouncerSwipeTouchHandler(
mKosmos.getTestScope(),
@@ -160,7 +185,10 @@
MIN_BOUNCER_HEIGHT,
mUiEventLogger,
mActivityStarter,
- mKeyguardInteractor);
+ mKeyguardInteractor,
+ mSceneInteractor,
+ Optional.of(() -> mWindowRootView)
+ );
when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -367,6 +395,7 @@
* Makes sure the expansion amount is proportional to (1 - scroll).
*/
@Test
+ @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
public void testSwipeUp_setsCorrectExpansionAmount() {
mTouchHandler.onSessionStart(mTouchSession);
ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
@@ -380,6 +409,36 @@
}
/**
+ * Makes sure that touches go to the scene container when the flag is on.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+ public void testSwipeUp_sendsTouchesToWindowRootView() {
+ mTouchHandler.onSessionStart(mTouchSession);
+ ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+ verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+ final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
+
+ final float distanceY = SCREEN_HEIGHT_PX * 0.42f;
+
+ final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+ 0, SCREEN_HEIGHT_PX, 0);
+ final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+ 0, SCREEN_HEIGHT_PX - distanceY, 0);
+
+ assertThat(gestureListener.onScroll(event1, event2, 0,
+ distanceY))
+ .isTrue();
+
+ // Ensure only called once
+ verify(mSceneInteractor).onRemoteUserInputStarted(any());
+ verify(mWindowRootView).dispatchTouchEvent(event1);
+ verify(mWindowRootView).dispatchTouchEvent(event2);
+ }
+
+ /**
* Verifies that swiping up when the lock pattern is not secure dismissed dream and consumes
* the gesture.
*/
@@ -476,6 +535,7 @@
* Tests that ending an upward swipe before the set threshold leads to bouncer collapsing down.
*/
@Test
+ @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
public void testSwipeUpPositionBelowThreshold_collapsesBouncer() {
final float swipeUpPercentage = .3f;
final float expansion = 1 - swipeUpPercentage;
@@ -499,6 +559,7 @@
* Tests that ending an upward swipe above the set threshold will continue the expansion.
*/
@Test
+ @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
public void testSwipeUpPositionAboveThreshold_expandsBouncer() {
final float swipeUpPercentage = .7f;
final float expansion = 1 - swipeUpPercentage;
@@ -528,6 +589,7 @@
* Tests that swiping up with a speed above the set threshold will continue the expansion.
*/
@Test
+ @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
public void testSwipeUpVelocityAboveMin_expandsBouncer() {
when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
index 38ea4497..fa5af51 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
@@ -18,9 +18,9 @@
import android.app.DreamManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.view.GestureDetector
import android.view.MotionEvent
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
@@ -28,14 +28,20 @@
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.sceneContainerRepository
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shared.system.InputChannelCompat
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.testKosmos
import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import java.util.Optional
+import javax.inject.Provider
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,22 +53,29 @@
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class ShadeTouchHandlerTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class ShadeTouchHandlerTest(flags: FlagsParameterization) : SysuiTestCase() {
private var kosmos = testKosmos()
private var mCentralSurfaces = mock<CentralSurfaces>()
private var mShadeViewController = mock<ShadeViewController>()
private var mDreamManager = mock<DreamManager>()
private var mTouchSession = mock<TouchSession>()
private var communalViewModel = mock<CommunalViewModel>()
+ private var windowRootView = mock<WindowRootView>()
private lateinit var mTouchHandler: ShadeTouchHandler
private var mGestureListenerCaptor = argumentCaptor<GestureDetector.OnGestureListener>()
private var mInputListenerCaptor = argumentCaptor<InputChannelCompat.InputEventListener>()
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Before
fun setup() {
mTouchHandler =
@@ -73,7 +86,9 @@
mDreamManager,
communalViewModel,
kosmos.communalSettingsInteractor,
- TOUCH_HEIGHT
+ kosmos.sceneInteractor,
+ Optional.of(Provider<WindowRootView> { windowRootView }),
+ TOUCH_HEIGHT,
)
}
@@ -97,7 +112,7 @@
// Verifies that a swipe down forwards captured touches to central surfaces for handling.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
fun testSwipeDown_communalEnabled_sentToCentralSurfaces() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -110,7 +125,11 @@
// Verifies that a swipe down forwards captured touches to the shade view for handling.
@Test
- @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(
+ Flags.FLAG_COMMUNAL_HUB,
+ Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+ Flags.FLAG_SCENE_CONTAINER,
+ )
fun testSwipeDown_communalDisabled_sentToShadeView() {
swipe(Direction.DOWN)
@@ -121,7 +140,7 @@
// Verifies that a swipe down while dreaming forwards captured touches to the shade view for
// handling.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
fun testSwipeDown_dreaming_sentToShadeView() {
whenever(mDreamManager.isDreaming).thenReturn(true)
swipe(Direction.DOWN)
@@ -130,9 +149,39 @@
verify(mShadeViewController, times(2)).handleExternalTouch(any())
}
+ // Verifies that a swipe down forwards captured touches to the window root view for handling.
+ @Test
+ @EnableFlags(
+ Flags.FLAG_COMMUNAL_HUB,
+ Flags.FLAG_SCENE_CONTAINER,
+ Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+ )
+ fun testSwipeDown_sceneContainerEnabled_sentToWindowRootView() {
+ mTouchHandler.onGlanceableTouchAvailable(true)
+
+ swipe(Direction.DOWN)
+
+ // Both motion events are sent for central surfaces to process.
+ assertThat(kosmos.sceneContainerRepository.isRemoteUserInputOngoing.value).isTrue()
+ verify(windowRootView, times(2)).dispatchTouchEvent(any())
+ }
+
+ // Verifies that a swipe down while dreaming forwards captured touches to the window root view
+ // for handling.
+ @Test
+ @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ fun testSwipeDown_sceneContainerEnabledFullscreenSwipeDisabled_sentToWindowRootView() {
+ swipe(Direction.DOWN)
+
+ // Both motion events are sent for the shade view to process.
+ assertThat(kosmos.sceneContainerRepository.isRemoteUserInputOngoing.value).isTrue()
+ verify(windowRootView, times(2)).dispatchTouchEvent(any())
+ }
+
// Verifies that a swipe up is not forwarded to central surfaces.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
fun testSwipeUp_communalEnabled_touchesNotSent() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -146,7 +195,11 @@
// Verifies that a swipe up is not forwarded to the shade view.
@Test
- @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(
+ Flags.FLAG_COMMUNAL_HUB,
+ Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+ Flags.FLAG_SCENE_CONTAINER,
+ )
fun testSwipeUp_communalDisabled_touchesNotSent() {
swipe(Direction.UP)
@@ -155,6 +208,17 @@
verify(mShadeViewController, never()).handleExternalTouch(any())
}
+ // Verifies that a swipe up is not forwarded to the window root view.
+ @Test
+ @EnableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_SCENE_CONTAINER)
+ fun testSwipeUp_sceneContainerEnabled_touchesNotSent() {
+ swipe(Direction.UP)
+
+ // Motion events are not sent for window root view to process as the swipe is going in the
+ // wrong direction.
+ verify(windowRootView, never()).dispatchTouchEvent(any())
+ }
+
@Test
@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testCancelMotionEvent_popsTouchSession() {
@@ -243,10 +307,16 @@
private enum class Direction {
DOWN,
- UP
+ UP,
}
companion object {
private const val TOUCH_HEIGHT = 20
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
index 22b114c..7816d3b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
@@ -366,6 +366,106 @@
assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
}
+ @Test
+ fun testCanExpand_atTopPosition_withMultipleAnchors_returnsTrue() =
+ testScope.runTest {
+ val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0)
+
+ updateGridLayout(twoRowGrid)
+ assertThat(underTest.canExpand()).isTrue()
+ assertThat(underTest.bottomDragState.anchors.toList())
+ .containsAtLeast(0 to 0f, 1 to 45f)
+ }
+
+ @Test
+ fun testCanExpand_atTopPosition_withSingleAnchors_returnsFalse() =
+ testScope.runTest {
+ val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0)
+ updateGridLayout(oneRowGrid)
+ assertThat(underTest.canExpand()).isFalse()
+ }
+
+ @Test
+ fun testCanExpand_atBottomPosition_withMultipleAnchors_returnsTrue() =
+ testScope.runTest {
+ val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1)
+ updateGridLayout(twoRowGrid)
+ assertThat(underTest.canExpand()).isTrue()
+ assertThat(underTest.topDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f)
+ }
+
+ @Test
+ fun testCanShrink_atMinimumHeight_returnsFalse() =
+ testScope.runTest {
+ val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0)
+ updateGridLayout(oneRowGrid)
+ assertThat(underTest.canShrink()).isFalse()
+ }
+
+ @Test
+ fun testCanShrink_atFullSize_checksBottomDragState() = runTestWithSnapshots {
+ val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0)
+ updateGridLayout(twoSpanGrid)
+
+ assertThat(underTest.canShrink()).isTrue()
+ assertThat(underTest.bottomDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f)
+ }
+
+ @Test
+ fun testResizeByAccessibility_expandFromBottom_usesTopDragState() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+ val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1)
+ updateGridLayout(twoSpanGrid)
+
+ underTest.expandToNextAnchor()
+
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.TOP))
+ }
+
+ @Test
+ fun testResizeByAccessibility_expandFromTop_usesBottomDragState() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+ val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0)
+ updateGridLayout(twoSpanGrid)
+
+ underTest.expandToNextAnchor()
+
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.BOTTOM))
+ }
+
+ @Test
+ fun testResizeByAccessibility_shrinkFromFull_usesBottomDragState() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+ val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0)
+ updateGridLayout(twoSpanGrid)
+
+ underTest.shrinkToNextAnchor()
+
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM))
+ }
+
+ @Test
+ fun testResizeByAccessibility_cannotResizeAtMinSize() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+ // Set up grid at minimum size
+ val minSizeGrid =
+ singleSpanGrid.copy(
+ totalSpans = 2,
+ currentSpan = 1,
+ minHeightPx = singleSpanGrid.minHeightPx,
+ currentRow = 0,
+ )
+ updateGridLayout(minSizeGrid)
+
+ underTest.shrinkToNextAnchor()
+
+ assertThat(resizeInfo).isNull()
+ }
+
@Test(expected = IllegalArgumentException::class)
fun testIllegalState_maxHeightLessThanMinHeight() =
testScope.runTest {
@@ -380,6 +480,24 @@
fun testIllegalState_resizeMultipleZeroOrNegative() =
testScope.runTest { updateGridLayout(singleSpanGrid.copy(resizeMultiple = 0)) }
+ @Test
+ fun testZeroHeights_cannotResize() = runTestWithSnapshots {
+ val zeroHeightGrid =
+ singleSpanGrid.copy(
+ totalSpans = 2,
+ currentSpan = 1,
+ currentRow = 0,
+ minHeightPx = 0,
+ maxHeightPx = 0,
+ )
+ updateGridLayout(zeroHeightGrid)
+
+ val topState = underTest.topDragState
+ val bottomState = underTest.bottomDragState
+ assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+ assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+ }
+
private fun TestScope.updateGridLayout(gridLayout: GridLayout) {
underTest.setGridLayoutInfo(
verticalItemSpacingPx = gridLayout.verticalItemSpacingPx,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt
index 1dd8ca9..6a0781b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt
@@ -20,9 +20,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
@@ -37,7 +36,7 @@
@SmallTest
class PerDisplayStoreImplTest : SysuiTestCase() {
- private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
private val fakeDisplayRepository = kosmos.displayRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
index 0145f17..4a422f0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
@@ -23,8 +23,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.unconfinedTestDispatcher
-import com.android.systemui.kosmos.unconfinedTestScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.res.R
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
@@ -33,7 +34,7 @@
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
+import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
@@ -51,10 +52,10 @@
@RunWith(AndroidJUnit4::class)
class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testDispatcher = kosmos.unconfinedTestDispatcher
- private val testScope = kosmos.unconfinedTestScope
- private val settings = kosmos.unconfinedDispatcherFakeSettings
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testDispatcher = kosmos.testDispatcher
+ private val testScope = kosmos.testScope
+ private val settings = kosmos.fakeSettings
@Mock private lateinit var sharedPrefs: FakeSharedPreferences
@@ -79,13 +80,7 @@
context = context,
userFileManager =
mock {
- whenever(
- getSharedPreferences(
- anyString(),
- anyInt(),
- anyInt(),
- )
- )
+ whenever(getSharedPreferences(anyString(), anyInt(), anyInt()))
.thenReturn(FakeSharedPreferences())
},
userTracker = FakeUserTracker(),
@@ -109,17 +104,14 @@
testScope.runTest {
val job = underTest.startSyncing()
- settings.putInt(
- Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
- 1,
- )
+ settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1)
assertThat(
selectionManager
.getSelections()
.getOrDefault(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
- emptyList()
+ emptyList(),
)
)
.contains(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
@@ -132,21 +124,15 @@
testScope.runTest {
val job = underTest.startSyncing()
- settings.putInt(
- Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
- 1,
- )
- settings.putInt(
- Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
- 0,
- )
+ settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1)
+ settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
assertThat(
selectionManager
.getSelections()
.getOrDefault(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
- emptyList()
+ emptyList(),
)
)
.doesNotContain(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
@@ -161,7 +147,7 @@
selectionManager.setSelections(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
- listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
+ listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET),
)
advanceUntilIdle()
@@ -177,11 +163,11 @@
selectionManager.setSelections(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
- listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET)
+ listOf(BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET),
)
selectionManager.setSelections(
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
- emptyList()
+ emptyList(),
)
assertThat(settings.getInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET)).isEqualTo(0)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index 83d2617..32fa160 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -20,6 +20,7 @@
import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.os.UserHandle
+import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
@@ -81,6 +82,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@DisableSceneContainer
+@FlakyTest(bugId = 292574995, detail = "NullPointer on MockMakerTypeMockability.mockable()")
class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index 785d5a8..02825a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -21,10 +21,13 @@
import android.os.Binder
import android.os.Handler
import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.ContentRecordingSession
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -74,7 +77,8 @@
}
@Test
- fun mediaProjectionState_onStart_emitsNotProjecting() =
+ @DisableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun mediaProjectionState_onStart_flagOff_emitsNotProjecting() =
testScope.runTest {
val state by collectLastValue(repo.mediaProjectionState)
@@ -84,6 +88,35 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun mediaProjectionState_onStart_flagOn_emitsProjectingNoScreen() =
+ testScope.runTest {
+ val state by collectLastValue(repo.mediaProjectionState)
+
+ fakeMediaProjectionManager.dispatchOnStart()
+
+ assertThat(state).isInstanceOf(MediaProjectionState.Projecting.NoScreen::class.java)
+ }
+
+ @Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun mediaProjectionState_noScreen_hasHostPackage() =
+ testScope.runTest {
+ val state by collectLastValue(repo.mediaProjectionState)
+
+ val info =
+ MediaProjectionInfo(
+ /* packageName= */ "com.media.projection.repository.test",
+ /* handle= */ UserHandle.getUserHandleForUid(UserHandle.myUserId()),
+ /* launchCookie = */ null,
+ )
+ fakeMediaProjectionManager.dispatchOnStart(info)
+
+ assertThat((state as MediaProjectionState.Projecting).hostPackage)
+ .isEqualTo("com.media.projection.repository.test")
+ }
+
+ @Test
fun mediaProjectionState_onStop_emitsNotProjecting() =
testScope.runTest {
val state by collectLastValue(repo.mediaProjectionState)
@@ -212,7 +245,7 @@
)
fakeMediaProjectionManager.dispatchOnSessionSet(
info = info,
- session = ContentRecordingSession.createTaskSession(token.asBinder())
+ session = ContentRecordingSession.createTaskSession(token.asBinder()),
)
assertThat((state as MediaProjectionState.Projecting.SingleTask).hostPackage)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 3850891..4995920 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -651,6 +651,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_TRANSITION_RACE_CONDITION)
fun switchToAOD_whenAvailable_whenDeviceSleepsLocked() =
testScope.runTest {
kosmos.lockscreenSceneTransitionInteractor.start()
@@ -680,6 +681,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_TRANSITION_RACE_CONDITION)
fun switchToDozing_whenAodUnavailable_whenDeviceSleepsLocked() =
testScope.runTest {
kosmos.lockscreenSceneTransitionInteractor.start()
@@ -701,6 +703,56 @@
}
@Test
+ @EnableFlags(Flags.FLAG_TRANSITION_RACE_CONDITION)
+ fun switchToAOD_whenAvailable_whenDeviceSleepsLocked_transitionFlagEnabled() =
+ testScope.runTest {
+ kosmos.lockscreenSceneTransitionInteractor.start()
+ val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState)
+ val transitionState =
+ prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Shade)
+ kosmos.keyguardRepository.setAodAvailable(true)
+ runCurrent()
+ assertThat(asleepState).isEqualTo(KeyguardState.AOD)
+ underTest.start()
+ powerInteractor.setAsleepForTest()
+ runCurrent()
+ transitionState.value =
+ ObservableTransitionState.Transition(
+ fromScene = Scenes.Shade,
+ toScene = Scenes.Lockscreen,
+ currentScene = flowOf(Scenes.Lockscreen),
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(false),
+ )
+ runCurrent()
+
+ assertThat(kosmos.keyguardTransitionRepository.currentTransitionInfo.to)
+ .isEqualTo(KeyguardState.AOD)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_TRANSITION_RACE_CONDITION)
+ fun switchToDozing_whenAodUnavailable_whenDeviceSleepsLocked_transitionFlagEnabled() =
+ testScope.runTest {
+ kosmos.lockscreenSceneTransitionInteractor.start()
+ val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState)
+ val transitionState =
+ prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Shade)
+ kosmos.keyguardRepository.setAodAvailable(false)
+ runCurrent()
+ assertThat(asleepState).isEqualTo(KeyguardState.DOZING)
+ underTest.start()
+ powerInteractor.setAsleepForTest()
+ runCurrent()
+ transitionState.value = Transition(from = Scenes.Shade, to = Scenes.Lockscreen)
+ runCurrent()
+
+ assertThat(kosmos.keyguardTransitionRepository.currentTransitionInfo.to)
+ .isEqualTo(KeyguardState.DOZING)
+ }
+
+ @Test
fun switchToGoneWhenDoubleTapPowerGestureIsTriggeredFromGone() =
testScope.runTest {
val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
index 5005d16..e33ce9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
@@ -92,7 +92,7 @@
createAndSetDelegate(
MediaProjectionState.Projecting.EntireScreen(
HOST_PACKAGE,
- hostDeviceName = "My Favorite Device"
+ hostDeviceName = "My Favorite Device",
)
)
@@ -118,8 +118,8 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1, baseIntent = baseIntent)
- ),
+ createTask(taskId = 1, baseIntent = baseIntent),
+ )
)
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -141,8 +141,8 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = "My Favorite Device",
- createTask(taskId = 1, baseIntent = baseIntent)
- ),
+ createTask(taskId = 1, baseIntent = baseIntent),
+ )
)
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -169,8 +169,8 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1, baseIntent = baseIntent)
- ),
+ createTask(taskId = 1, baseIntent = baseIntent),
+ )
)
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -198,7 +198,7 @@
HOST_PACKAGE,
hostDeviceName = "My Favorite Device",
createTask(taskId = 1, baseIntent = baseIntent),
- ),
+ )
)
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
@@ -235,7 +235,7 @@
verify(sysuiDialog)
.setPositiveButton(
eq(R.string.cast_to_other_device_stop_dialog_button),
- clickListener.capture()
+ clickListener.capture(),
)
// Verify that clicking the button stops the recording
@@ -254,7 +254,8 @@
kosmos.applicationContext,
stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
ProjectionChipModel.Projecting(
- ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE,
+ ProjectionChipModel.Receiver.CastToOtherDevice,
+ ProjectionChipModel.ContentType.Screen,
state,
),
)
@@ -268,7 +269,7 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1)
+ createTask(taskId = 1),
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index 77992db..01e5501 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -17,9 +17,11 @@
package com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel
import android.content.DialogInterface
+import android.platform.test.annotations.EnableFlags
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.jank.Cuj
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.mockDialogTransitionAnimator
@@ -135,6 +137,29 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_projectionIsAudioOnly_otherDevicePackage_isShownAsIconOnly() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaRouterRepo.castDevices.value = emptyList()
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(
+ hostPackage = CAST_TO_OTHER_DEVICES_PACKAGE
+ )
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+ val icon =
+ (((latest as OngoingActivityChipModel.Shown).icon)
+ as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
+ .impl as Icon.Resource
+ assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected)
+ // This content description is just generic "Casting", not "Casting screen"
+ assertThat((icon.contentDescription as ContentDescription.Resource).res)
+ .isEqualTo(R.string.accessibility_casting)
+ }
+
+ @Test
fun chip_projectionIsEntireScreenState_otherDevicesPackage_isShownAsTimer_forScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -292,6 +317,18 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_projectionIsNoScreenState_normalPackage_isHidden() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ }
+
+ @Test
fun chip_projectionIsSingleTaskState_normalPackage_isHidden() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -387,12 +424,7 @@
clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- eq(mockScreenCastDialog),
- eq(chipBackgroundView),
- any(),
- anyBoolean(),
- )
+ .showFromView(eq(mockScreenCastDialog), eq(chipBackgroundView), any(), anyBoolean())
}
@Test
@@ -412,12 +444,7 @@
clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- eq(mockScreenCastDialog),
- eq(chipBackgroundView),
- any(),
- anyBoolean(),
- )
+ .showFromView(eq(mockScreenCastDialog), eq(chipBackgroundView), any(), anyBoolean())
}
@Test
@@ -461,12 +488,7 @@
val cujCaptor = argumentCaptor<DialogCuj>()
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- any(),
- any(),
- cujCaptor.capture(),
- anyBoolean(),
- )
+ .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
assertThat(cujCaptor.firstValue.cujType)
.isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
@@ -494,12 +516,7 @@
val cujCaptor = argumentCaptor<DialogCuj>()
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- any(),
- any(),
- cujCaptor.capture(),
- anyBoolean(),
- )
+ .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
assertThat(cujCaptor.firstValue.cujType)
.isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
index d0c5e7a..611318a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
@@ -21,7 +21,9 @@
import android.content.packageManager
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
+import android.platform.test.annotations.EnableFlags
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -65,7 +67,23 @@
}
@Test
- fun projection_singleTaskState_otherDevicesPackage_isCastToOtherDeviceType() =
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun projection_noScreenState_otherDevicesPackage_isCastToOtherAndAudio() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.projection)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+ assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Audio)
+ }
+
+ @Test
+ fun projection_singleTaskState_otherDevicesPackage_isCastToOtherAndScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.projection)
@@ -73,31 +91,49 @@
MediaProjectionState.Projecting.SingleTask(
CAST_TO_OTHER_DEVICES_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1)
+ createTask(taskId = 1),
)
assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
- assertThat((latest as ProjectionChipModel.Projecting).type)
- .isEqualTo(ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Screen)
}
@Test
- fun projection_entireScreenState_otherDevicesPackage_isCastToOtherDeviceChipType() =
+ fun projection_entireScreenState_otherDevicesPackage_isCastToOtherAndScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.projection)
mediaProjectionRepo.mediaProjectionState.value =
- MediaProjectionState.Projecting.EntireScreen(
- CAST_TO_OTHER_DEVICES_PACKAGE,
- )
+ MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
- assertThat((latest as ProjectionChipModel.Projecting).type)
- .isEqualTo(ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.CastToOtherDevice)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Screen)
}
@Test
- fun projection_singleTaskState_normalPackage_isShareToAppChipType() =
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun projection_noScreenState_normalPackage_isShareToAppAndAudio() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.projection)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+ assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Audio)
+ }
+
+ @Test
+ fun projection_singleTaskState_normalPackage_isShareToAppAndScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.projection)
@@ -109,12 +145,14 @@
)
assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
- assertThat((latest as ProjectionChipModel.Projecting).type)
- .isEqualTo(ProjectionChipModel.Type.SHARE_TO_APP)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Screen)
}
@Test
- fun projection_entireScreenState_normalPackage_isShareToAppChipType() =
+ fun projection_entireScreenState_normalPackage_isShareToAppAndScreen() =
testScope.runTest {
val latest by collectLastValue(underTest.projection)
@@ -122,8 +160,10 @@
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
assertThat(latest).isInstanceOf(ProjectionChipModel.Projecting::class.java)
- assertThat((latest as ProjectionChipModel.Projecting).type)
- .isEqualTo(ProjectionChipModel.Type.SHARE_TO_APP)
+ assertThat((latest as ProjectionChipModel.Projecting).receiver)
+ .isEqualTo(ProjectionChipModel.Receiver.ShareToApp)
+ assertThat((latest as ProjectionChipModel.Projecting).contentType)
+ .isEqualTo(ProjectionChipModel.ContentType.Screen)
}
companion object {
@@ -140,14 +180,14 @@
whenever(
this.checkPermission(
Manifest.permission.REMOTE_DISPLAY_PROVIDER,
- CAST_TO_OTHER_DEVICES_PACKAGE
+ CAST_TO_OTHER_DEVICES_PACKAGE,
)
)
.thenReturn(PackageManager.PERMISSION_GRANTED)
whenever(
this.checkPermission(
Manifest.permission.REMOTE_DISPLAY_PROVIDER,
- NORMAL_PACKAGE
+ NORMAL_PACKAGE,
)
)
.thenReturn(PackageManager.PERMISSION_DENIED)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
new file mode 100644
index 0000000..411d306
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.sharetoapp.ui.view
+
+import android.content.DialogInterface
+import android.content.applicationContext
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.endMediaProjectionDialogHelper
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndGenericShareToAppDialogDelegateTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val sysuiDialog = mock<SystemUIDialog>()
+ private val underTest =
+ EndGenericShareToAppDialogDelegate(
+ kosmos.endMediaProjectionDialogHelper,
+ kosmos.applicationContext,
+ stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
+ )
+
+ @Test
+ fun positiveButton_clickStopsRecording() =
+ kosmos.testScope.runTest {
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse()
+
+ val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+ verify(sysuiDialog).setPositiveButton(any(), clickListener.capture())
+ clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+ runCurrent()
+
+ assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
similarity index 93%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
index 325a42b..6885a6b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
@@ -50,10 +50,10 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
-class EndShareToAppDialogDelegateTest : SysuiTestCase() {
+class EndShareScreenToAppDialogDelegateTest : SysuiTestCase() {
private val kosmos = Kosmos().also { it.testCase = this }
private val sysuiDialog = mock<SystemUIDialog>()
- private lateinit var underTest: EndShareToAppDialogDelegate
+ private lateinit var underTest: EndShareScreenToAppDialogDelegate
@Test
fun icon() {
@@ -117,7 +117,7 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1, baseIntent = baseIntent)
+ createTask(taskId = 1, baseIntent = baseIntent),
)
)
@@ -142,7 +142,7 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1, baseIntent = baseIntent)
+ createTask(taskId = 1, baseIntent = baseIntent),
)
)
@@ -181,7 +181,7 @@
verify(sysuiDialog)
.setPositiveButton(
eq(R.string.share_to_app_stop_dialog_button),
- clickListener.capture()
+ clickListener.capture(),
)
// Verify that clicking the button stops the recording
@@ -195,12 +195,13 @@
private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
underTest =
- EndShareToAppDialogDelegate(
+ EndShareScreenToAppDialogDelegate(
kosmos.endMediaProjectionDialogHelper,
kosmos.applicationContext,
stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
ProjectionChipModel.Projecting(
- ProjectionChipModel.Type.SHARE_TO_APP,
+ ProjectionChipModel.Receiver.ShareToApp,
+ ProjectionChipModel.ContentType.Screen,
state,
),
)
@@ -213,7 +214,7 @@
MediaProjectionState.Projecting.SingleTask(
HOST_PACKAGE,
hostDeviceName = null,
- createTask(taskId = 1)
+ createTask(taskId = 1),
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 791a21d..d7d57c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -17,12 +17,15 @@
package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
import android.content.DialogInterface
+import android.platform.test.annotations.EnableFlags
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.jank.Cuj
+import com.android.systemui.Flags.FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.mockDialogTransitionAnimator
+import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -35,7 +38,8 @@
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.CAST_TO_OTHER_DEVICES_PACKAGE
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
-import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareScreenToAppDialogDelegate
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
@@ -62,7 +66,8 @@
private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
private val systemClock = kosmos.fakeSystemClock
- private val mockShareDialog = mock<SystemUIDialog>()
+ private val mockScreenShareDialog = mock<SystemUIDialog>()
+ private val mockGenericShareDialog = mock<SystemUIDialog>()
private val chipBackgroundView = mock<ChipBackgroundContainer>()
private val chipView =
mock<View>().apply {
@@ -80,8 +85,10 @@
fun setUp() {
setUpPackageManagerForMediaProjection(kosmos)
- whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareToAppDialogDelegate>()))
- .thenReturn(mockShareDialog)
+ whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareScreenToAppDialogDelegate>()))
+ .thenReturn(mockScreenShareDialog)
+ whenever(kosmos.mockSystemUIDialogFactory.create(any<EndGenericShareToAppDialogDelegate>()))
+ .thenReturn(mockGenericShareDialog)
}
@Test
@@ -95,6 +102,21 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_noScreenState_otherDevicesPackage_isHidden() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(
+ CAST_TO_OTHER_DEVICES_PACKAGE,
+ hostDeviceName = null,
+ )
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ }
+
+ @Test
fun chip_singleTaskState_otherDevicesPackage_isHidden() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -121,6 +143,26 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_noScreenState_normalPackage_isShownAsIconOnly() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE, hostDeviceName = null)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java)
+ val icon =
+ (((latest as OngoingActivityChipModel.Shown).icon)
+ as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
+ .impl as Icon.Resource
+ assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
+ // This content description is just generic "Sharing content", not "Sharing screen"
+ assertThat((icon.contentDescription as ContentDescription.Resource).res)
+ .isEqualTo(R.string.share_to_app_chip_accessibility_label_generic)
+ }
+
+ @Test
fun chip_singleTaskState_normalPackage_isShownAsTimer() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -170,7 +212,7 @@
// WHEN the stop action on the dialog is clicked
val dialogStopAction =
- getStopActionFromDialog(latest, chipView, mockShareDialog, kosmos)
+ getStopActionFromDialog(latest, chipView, mockScreenShareDialog, kosmos)
dialogStopAction.onClick(mock<DialogInterface>(), 0)
// THEN the chip is immediately hidden...
@@ -222,7 +264,28 @@
}
@Test
- fun chip_entireScreen_clickListenerShowsShareDialog() =
+ @EnableFlags(FLAG_STATUS_BAR_SHOW_AUDIO_ONLY_PROJECTION_CHIP)
+ fun chip_noScreen_clickListenerShowsGenericShareDialog() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.NoScreen(NORMAL_PACKAGE)
+
+ val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
+
+ clickListener!!.onClick(chipView)
+ verify(kosmos.mockDialogTransitionAnimator)
+ .showFromView(
+ eq(mockGenericShareDialog),
+ eq(chipBackgroundView),
+ any(),
+ anyBoolean(),
+ )
+ }
+
+ @Test
+ fun chip_entireScreen_clickListenerShowsScreenShareDialog() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
@@ -234,7 +297,7 @@
clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
- eq(mockShareDialog),
+ eq(mockScreenShareDialog),
eq(chipBackgroundView),
any(),
anyBoolean(),
@@ -242,7 +305,7 @@
}
@Test
- fun chip_singleTask_clickListenerShowsShareDialog() =
+ fun chip_singleTask_clickListenerShowsScreenShareDialog() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
@@ -258,7 +321,7 @@
clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
- eq(mockShareDialog),
+ eq(mockScreenShareDialog),
eq(chipBackgroundView),
any(),
anyBoolean(),
@@ -281,12 +344,7 @@
val cujCaptor = argumentCaptor<DialogCuj>()
verify(kosmos.mockDialogTransitionAnimator)
- .showFromView(
- any(),
- any(),
- cujCaptor.capture(),
- anyBoolean(),
- )
+ .showFromView(any(), any(), cujCaptor.capture(), anyBoolean())
assertThat(cujCaptor.firstValue.cujType)
.isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
index 7923097..659e53f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/StatusBarOrchestratorTest.kt
@@ -22,9 +22,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.plugins.DarkIconDispatcher
import com.android.systemui.plugins.mockPluginDependencyProvider
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -57,7 +56,7 @@
@RunWith(AndroidJUnit4::class)
class StatusBarOrchestratorTest : SysuiTestCase() {
- private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
private val fakeStatusBarModePerDisplayRepository = kosmos.fakeStatusBarModePerDisplayRepository
private val mockPluginDependencyProvider = kosmos.mockPluginDependencyProvider
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt
index 0eebab0..4a26fdf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/MultiDisplayStatusBarContentInsetsProviderStoreTest.kt
@@ -21,9 +21,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.displayRepository
-import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
@@ -37,7 +36,7 @@
@RunWith(AndroidJUnit4::class)
class MultiDisplayStatusBarContentInsetsProviderStoreTest : SysuiTestCase() {
- private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
private val fakeDisplayRepository = kosmos.displayRepository
private val underTest = kosmos.multiDisplayStatusBarContentInsetsProviderStore
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 1af0f79..b03c679 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -24,14 +24,15 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.kosmos.unconfinedTestDispatcher
-import com.android.systemui.kosmos.unconfinedTestScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.model.SelectedUserModel
import com.android.systemui.user.data.model.SelectionStatus
import com.android.systemui.user.data.model.UserSwitcherSettingsModel
-import com.android.systemui.util.settings.unconfinedDispatcherFakeGlobalSettings
+import com.android.systemui.util.settings.fakeGlobalSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -52,10 +53,10 @@
@RunWith(AndroidJUnit4::class)
class UserRepositoryImplTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testDispatcher = kosmos.unconfinedTestDispatcher
- private val testScope = kosmos.unconfinedTestScope
- private val globalSettings = kosmos.unconfinedDispatcherFakeGlobalSettings
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testDispatcher = kosmos.testDispatcher
+ private val testScope = kosmos.testScope
+ private val globalSettings = kosmos.fakeGlobalSettings
@Mock private lateinit var manager: UserManager
@@ -131,11 +132,7 @@
whenever(mainUser.identifier).thenReturn(mainUserId)
underTest = create(testScope.backgroundScope)
- val initialExpectedValue =
- setUpUsers(
- count = 3,
- selectedIndex = 0,
- )
+ val initialExpectedValue = setUpUsers(count = 3, selectedIndex = 0)
var userInfos: List<UserInfo>? = null
var selectedUserInfo: UserInfo? = null
val job1 = underTest.userInfos.onEach { userInfos = it }.launchIn(this)
@@ -146,11 +143,7 @@
assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
- val secondExpectedValue =
- setUpUsers(
- count = 4,
- selectedIndex = 1,
- )
+ val secondExpectedValue = setUpUsers(count = 4, selectedIndex = 1)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(secondExpectedValue)
assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
@@ -158,11 +151,7 @@
val selectedNonGuestUserId = selectedUserInfo?.id
val thirdExpectedValue =
- setUpUsers(
- count = 2,
- isLastGuestUser = true,
- selectedIndex = 1,
- )
+ setUpUsers(count = 2, isLastGuestUser = true, selectedIndex = 1)
underTest.refreshUsers()
assertThat(userInfos).isEqualTo(thirdExpectedValue)
assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
@@ -177,12 +166,7 @@
fun refreshUsers_sortsByCreationTime_guestUserLast() =
testScope.runTest {
underTest = create(testScope.backgroundScope)
- val unsortedUsers =
- setUpUsers(
- count = 3,
- selectedIndex = 0,
- isLastGuestUser = true,
- )
+ val unsortedUsers = setUpUsers(count = 3, selectedIndex = 0, isLastGuestUser = true)
unsortedUsers[0].creationTime = 999
unsortedUsers[1].creationTime = 900
unsortedUsers[2].creationTime = 950
@@ -207,10 +191,7 @@
): List<UserInfo> {
val userInfos =
(0 until count).map { index ->
- createUserInfo(
- index,
- isGuest = isLastGuestUser && index == count - 1,
- )
+ createUserInfo(index, isGuest = isLastGuestUser && index == count - 1)
}
whenever(manager.aliveUsers).thenReturn(userInfos)
tracker.set(userInfos, selectedIndex)
@@ -224,16 +205,10 @@
var selectedUserInfo: UserInfo? = null
val job = underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
- setUpUsers(
- count = 2,
- selectedIndex = 0,
- )
+ setUpUsers(count = 2, selectedIndex = 0)
tracker.onProfileChanged()
assertThat(selectedUserInfo?.id).isEqualTo(0)
- setUpUsers(
- count = 2,
- selectedIndex = 1,
- )
+ setUpUsers(count = 2, selectedIndex = 1)
tracker.onProfileChanged()
assertThat(selectedUserInfo?.id).isEqualTo(1)
job.cancel()
@@ -287,10 +262,7 @@
job.cancel()
}
- private fun createUserInfo(
- id: Int,
- isGuest: Boolean,
- ): UserInfo {
+ private fun createUserInfo(id: Int, isGuest: Boolean): UserInfo {
val flags = 0
return UserInfo(
id,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
index faf01ed..1e6e52a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
@@ -30,6 +30,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.fakeVolumeDialogController
import com.android.systemui.testKosmos
+import com.android.systemui.volume.data.repository.audioSystemRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -65,8 +66,8 @@
setUpRingerModeAndOpenDrawer(normalRingerMode)
- assertThat(ringerViewModel).isNotNull()
- assertThat(ringerViewModel?.drawerState)
+ assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
+ assertThat((ringerViewModel as RingerViewModelState.Available).uiModel.drawerState)
.isEqualTo(RingerDrawerState.Open(normalRingerMode))
}
@@ -80,8 +81,8 @@
underTest.onRingerButtonClicked(normalRingerMode)
controller.getState()
- assertThat(ringerViewModel).isNotNull()
- assertThat(ringerViewModel?.drawerState)
+ assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
+ assertThat((ringerViewModel as RingerViewModelState.Available).uiModel.drawerState)
.isEqualTo(RingerDrawerState.Closed(normalRingerMode))
}
@@ -97,16 +98,12 @@
controller.getState()
runCurrent()
- assertThat(ringerViewModel).isNotNull()
- assertThat(
- ringerViewModel
- ?.availableButtons
- ?.get(ringerViewModel!!.currentButtonIndex)
- ?.ringerMode
- )
+ assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
+
+ var uiModel = (ringerViewModel as RingerViewModelState.Available).uiModel
+ assertThat(uiModel.availableButtons[uiModel.currentButtonIndex]?.ringerMode)
.isEqualTo(vibrateRingerMode)
- assertThat(ringerViewModel?.drawerState)
- .isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))
+ assertThat(uiModel.drawerState).isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))
val silentRingerMode = RingerMode(RINGER_MODE_SILENT)
// Open drawer
@@ -118,27 +115,48 @@
controller.getState()
runCurrent()
- assertThat(ringerViewModel).isNotNull()
- assertThat(
- ringerViewModel
- ?.availableButtons
- ?.get(ringerViewModel!!.currentButtonIndex)
- ?.ringerMode
- )
+ assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
+
+ uiModel = (ringerViewModel as RingerViewModelState.Available).uiModel
+ assertThat(uiModel.availableButtons[uiModel.currentButtonIndex]?.ringerMode)
.isEqualTo(silentRingerMode)
- assertThat(ringerViewModel?.drawerState)
- .isEqualTo(RingerDrawerState.Closed(silentRingerMode))
+ assertThat(uiModel.drawerState).isEqualTo(RingerDrawerState.Closed(silentRingerMode))
assertThat(controller.hasScheduledTouchFeedback).isFalse()
assertThat(vibratorHelper.totalVibrations).isEqualTo(2)
}
- private fun TestScope.setUpRingerModeAndOpenDrawer(selectedRingerMode: RingerMode) {
- controller.setStreamVolume(STREAM_RING, 50)
- controller.setRingerMode(selectedRingerMode.value, false)
- runCurrent()
+ @Test
+ fun onVolumeSingleMode_ringerIsUnavailable() =
+ testScope.runTest {
+ val ringerViewModel by collectLastValue(underTest.ringerViewModel)
+ kosmos.audioSystemRepository.setIsSingleVolume(true)
+ setUpRingerMode(RingerMode(RINGER_MODE_NORMAL))
+
+ assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Unavailable::class.java)
+ }
+
+ @Test
+ fun setUnsupportedRingerMode_ringerIsUnavailable() =
+ testScope.runTest {
+ val ringerViewModel by collectLastValue(underTest.ringerViewModel)
+
+ controller.setHasVibrator(false)
+ setUpRingerMode(RingerMode(RINGER_MODE_VIBRATE))
+
+ assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Unavailable::class.java)
+ }
+
+ private fun TestScope.setUpRingerModeAndOpenDrawer(selectedRingerMode: RingerMode) {
+ setUpRingerMode(selectedRingerMode)
underTest.onRingerButtonClicked(RingerMode(selectedRingerMode.value))
controller.getState()
runCurrent()
}
+
+ private fun TestScope.setUpRingerMode(selectedRingerMode: RingerMode) {
+ controller.setStreamVolume(STREAM_RING, 50)
+ controller.setRingerMode(selectedRingerMode.value, false)
+ runCurrent()
+ }
}
diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
index 84f39af..d16017a 100644
--- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
+++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceConfigPlugin.kt
@@ -23,4 +23,6 @@
val isDefaultDateWeatherDisabled: Boolean
/** Gets if Smartspace should use ViewPager2 */
val isViewPager2Enabled: Boolean
+ /** Gets if card swipe event should be logged */
+ val isSwipeEventLoggingEnabled: Boolean
}
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0aa5ccf..fe720b9 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -342,8 +342,12 @@
<!-- Content description for the status bar chip shown to the user when they're sharing their screen to another app on the device [CHAR LIMIT=NONE] -->
<string name="share_to_app_chip_accessibility_label">Sharing screen</string>
+ <!-- Content description for the status bar chip shown to the user when they're sharing their screen or audio to another app on the device [CHAR LIMIT=NONE] -->
+ <string name="share_to_app_chip_accessibility_label_generic">Sharing content</string>
<!-- Title for a dialog shown to the user that will let them stop sharing their screen to another app on the device [CHAR LIMIT=50] -->
<string name="share_to_app_stop_dialog_title">Stop sharing screen?</string>
+ <!-- Title for a dialog shown to the user that will let them stop sharing their screen or audio to another app on the device [CHAR LIMIT=50] -->
+ <string name="share_to_app_stop_dialog_title_generic">Stop sharing?</string>
<!-- Text telling a user that they're currently sharing their entire screen to [host_app_name] (i.e. [host_app_name] can currently see all screen content) [CHAR LIMIT=150] -->
<string name="share_to_app_stop_dialog_message_entire_screen_with_host_app">You\'re currently sharing your entire screen with <xliff:g id="host_app_name" example="Screen Recorder App">%1$s</xliff:g></string>
<!-- Text telling a user that they're currently sharing their entire screen to an app (but we don't know what app) [CHAR LIMIT=150] -->
@@ -352,6 +356,8 @@
<string name="share_to_app_stop_dialog_message_single_app_specific">You\'re currently sharing <xliff:g id="app_being_shared_name" example="Photos App">%1$s</xliff:g></string>
<!-- Text telling a user that they're currently sharing their screen [CHAR LIMIT=150] -->
<string name="share_to_app_stop_dialog_message_single_app_generic">You\'re currently sharing an app</string>
+ <!-- Text telling a user that they're currently sharing something to an app [CHAR LIMIT=100] -->
+ <string name="share_to_app_stop_dialog_message_generic">You\'re currently sharing with an app</string>
<!-- Button to stop screen sharing [CHAR LIMIT=35] -->
<string name="share_to_app_stop_dialog_button">Stop sharing</string>
@@ -1311,6 +1317,10 @@
<string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string>
<!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] -->
<string name="accessibility_action_label_unselect_widget">unselect widget</string>
+ <!-- Label for accessibility action to shrink a widget in edit mode. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_label_shrink_widget">Decrease height</string>
+ <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_label_expand_widget">Increase height</string>
<!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
<string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
<!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
index 223a21d..e365b77 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
@@ -27,6 +27,7 @@
import android.view.MotionEvent
import android.view.VelocityTracker
import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.systemui.Flags
@@ -38,6 +39,9 @@
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -45,12 +49,12 @@
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
+import javax.inject.Provider
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Monitor for tracking touches on the DreamOverlay to bring up the bouncer. */
class BouncerSwipeTouchHandler
@@ -74,6 +78,8 @@
private val uiEventLogger: UiEventLogger,
private val activityStarter: ActivityStarter,
private val keyguardInteractor: KeyguardInteractor,
+ private val sceneInteractor: SceneInteractor,
+ private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
) : TouchHandler {
/** An interface for creating ValueAnimators. */
interface ValueAnimatorCreator {
@@ -100,6 +106,8 @@
currentScrimController = controller
}
+ private val windowRootView by lazy { windowRootViewProvider.get().get() }
+
/** Determines whether the touch handler should process touches in fullscreen swiping mode */
private var touchAvailable = false
@@ -109,7 +117,7 @@
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
- distanceY: Float
+ distanceY: Float,
): Boolean {
if (capture == null) {
capture =
@@ -128,6 +136,11 @@
expanded = false
// Since the user is dragging the bouncer up, set scrimmed to false.
currentScrimController?.show()
+
+ if (SceneContainerFlag.isEnabled) {
+ sceneInteractor.onRemoteUserInputStarted("bouncer touch handler")
+ e1?.apply { windowRootView.dispatchTouchEvent(e1) }
+ }
}
}
if (capture != true) {
@@ -152,20 +165,27 @@
/* cancelAction= */ null,
/* dismissShade= */ true,
/* afterKeyguardGone= */ true,
- /* deferred= */ false
+ /* deferred= */ false,
)
return true
}
- // For consistency, we adopt the expansion definition found in the
- // PanelViewController. In this case, expansion refers to the view above the
- // bouncer. As that view's expansion shrinks, the bouncer appears. The bouncer
- // is fully hidden at full expansion (1) and fully visible when fully collapsed
- // (0).
- touchSession?.apply {
- val screenTravelPercentage =
- (abs((this@outer.y - e2.y).toDouble()) / getBounds().height()).toFloat()
- setPanelExpansion(1 - screenTravelPercentage)
+ if (SceneContainerFlag.isEnabled) {
+ windowRootView.dispatchTouchEvent(e2)
+ } else {
+ // For consistency, we adopt the expansion definition found in the
+ // PanelViewController. In this case, expansion refers to the view above the
+ // bouncer. As that view's expansion shrinks, the bouncer appears. The
+ // bouncer
+ // is fully hidden at full expansion (1) and fully visible when fully
+ // collapsed
+ // (0).
+ touchSession?.apply {
+ val screenTravelPercentage =
+ (abs((this@outer.y - e2.y).toDouble()) / getBounds().height())
+ .toFloat()
+ setPanelExpansion(1 - screenTravelPercentage)
+ }
}
}
@@ -194,7 +214,7 @@
ShadeExpansionChangeEvent(
/* fraction= */ currentExpansion,
/* expanded= */ expanded,
- /* tracking= */ true
+ /* tracking= */ true,
)
currentScrimController?.expand(event)
}
@@ -347,7 +367,7 @@
currentHeight,
targetHeight,
velocity,
- viewHeight
+ viewHeight,
)
} else {
// Shows the bouncer, i.e., fully collapses the space above the bouncer.
@@ -356,7 +376,7 @@
currentHeight,
targetHeight,
velocity,
- viewHeight
+ viewHeight,
)
}
animator.start()
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
index 1951a23..50e62a8 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
@@ -22,19 +22,23 @@
import android.view.InputEvent
import android.view.MotionEvent
import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.Flags
import com.android.systemui.ambient.touch.TouchHandler.TouchSession
import com.android.systemui.ambient.touch.dagger.ShadeModule
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.phone.CentralSurfaces
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
+import javax.inject.Provider
import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/**
* [ShadeTouchHandler] is responsible for handling swipe down gestures over dream to bring down the
@@ -49,8 +53,10 @@
private val dreamManager: DreamManager,
private val communalViewModel: CommunalViewModel,
private val communalSettingsInteractor: CommunalSettingsInteractor,
+ private val sceneInteractor: SceneInteractor,
+ private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
@param:Named(ShadeModule.NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT)
- private val initiationHeight: Int
+ private val initiationHeight: Int,
) : TouchHandler {
/**
* Tracks whether or not we are capturing a given touch. Will be null before and after a touch.
@@ -60,6 +66,8 @@
/** Determines whether the touch handler should process touches in fullscreen swiping mode */
private var touchAvailable = false
+ private val windowRootView by lazy { windowRootViewProvider.get().get() }
+
init {
if (Flags.hubmodeFullscreenVerticalSwipeFix()) {
scope.launch {
@@ -100,7 +108,7 @@
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
- distanceY: Float
+ distanceY: Float,
): Boolean {
if (capture == null) {
// Only capture swipes that are going downwards.
@@ -110,6 +118,10 @@
if (Flags.hubmodeFullscreenVerticalSwipeFix()) touchAvailable
else true
if (capture == true) {
+ if (SceneContainerFlag.isEnabled) {
+ sceneInteractor.onRemoteUserInputStarted("shade touch handler")
+ }
+
// Send the initial touches over, as the input listener has already
// processed these touches.
e1?.apply { sendTouchEvent(this) }
@@ -123,7 +135,7 @@
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
- velocityY: Float
+ velocityY: Float,
): Boolean {
return capture == true
}
@@ -132,6 +144,11 @@
}
private fun sendTouchEvent(event: MotionEvent) {
+ if (SceneContainerFlag.isEnabled) {
+ windowRootView.dispatchTouchEvent(event)
+ return
+ }
+
if (communalSettingsInteractor.isCommunalFlagEnabled() && !dreamManager.isDreaming) {
// Send touches to central surfaces only when on the glanceable hub while not dreaming.
// While sending touches where while dreaming will open the shade, the shade
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
index bc2f354..1c781d6 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
@@ -22,8 +22,10 @@
import com.android.systemui.ambient.touch.TouchHandler;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.res.R;
+import com.android.systemui.scene.ui.view.WindowRootView;
import dagger.Binds;
+import dagger.BindsOptionalOf;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
@@ -51,6 +53,13 @@
ShadeTouchHandler touchHandler);
/**
+ * Window root view is used to send touches to the scene container. Declaring as optional as it
+ * may not be present on all SysUI variants.
+ */
+ @BindsOptionalOf
+ abstract WindowRootView bindWindowRootView();
+
+ /**
* Provides the height of the gesture area for notification swipe down.
*/
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
index db4bee7..bde5d0f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
@@ -17,6 +17,7 @@
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.foundation.gestures.snapTo
import androidx.compose.runtime.snapshotFlow
import com.android.app.tracing.coroutines.coroutineScopeTraced as coroutineScope
import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -81,6 +82,72 @@
get() = roundDownToMultiple(getSpansForPx(minHeightPx))
}
+ /** Check if widget can expanded based on current drag states */
+ fun canExpand(): Boolean {
+ return getNextAnchor(bottomDragState, moveUp = false) != null ||
+ getNextAnchor(topDragState, moveUp = true) != null
+ }
+
+ /** Check if widget can shrink based on current drag states */
+ fun canShrink(): Boolean {
+ return getNextAnchor(bottomDragState, moveUp = true) != null ||
+ getNextAnchor(topDragState, moveUp = false) != null
+ }
+
+ /** Get the next anchor value in the specified direction */
+ private fun getNextAnchor(state: AnchoredDraggableState<Int>, moveUp: Boolean): Int? {
+ var nextAnchor: Int? = null
+ var nextAnchorDiff = Int.MAX_VALUE
+ val currentValue = state.currentValue
+
+ for (i in 0 until state.anchors.size) {
+ val anchor = state.anchors.anchorAt(i) ?: continue
+ if (anchor == currentValue) continue
+
+ val diff =
+ if (moveUp) {
+ currentValue - anchor
+ } else {
+ anchor - currentValue
+ }
+
+ if (diff in 1..<nextAnchorDiff) {
+ nextAnchor = anchor
+ nextAnchorDiff = diff
+ }
+ }
+
+ return nextAnchor
+ }
+
+ /** Handle expansion to the next anchor */
+ suspend fun expandToNextAnchor() {
+ if (!canExpand()) return
+ val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = false)
+ if (bottomAnchor != null) {
+ bottomDragState.snapTo(bottomAnchor)
+ return
+ }
+ val topAnchor =
+ getNextAnchor(
+ state = topDragState,
+ moveUp = true, // Moving up to expand
+ )
+ topAnchor?.let { topDragState.snapTo(it) }
+ }
+
+ /** Handle shrinking to the next anchor */
+ suspend fun shrinkToNextAnchor() {
+ if (!canShrink()) return
+ val topAnchor = getNextAnchor(state = topDragState, moveUp = false)
+ if (topAnchor != null) {
+ topDragState.snapTo(topAnchor)
+ return
+ }
+ val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = true)
+ bottomAnchor?.let { bottomDragState.snapTo(it) }
+ }
+
/**
* The layout information necessary in order to calculate the pixel offsets of the drag anchor
* points.
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index c6be0dd..b966ad4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -35,6 +35,7 @@
import com.android.systemui.dock.DockManagerImpl;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.education.dagger.ContextualEducationModule;
+import com.android.systemui.emergency.EmergencyGestureModule;
import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule;
import com.android.systemui.keyboard.shortcut.ShortcutHelperModule;
import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule;
@@ -123,6 +124,7 @@
CollapsedStatusBarFragmentStartableModule.class,
ConnectingDisplayViewModel.StartableModule.class,
DefaultBlueprintModule.class,
+ EmergencyGestureModule.class,
GestureModule.class,
HeadsUpModule.class,
KeyboardShortcutsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 7a6ca08..1ffbbd2 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -65,7 +65,6 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
-import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -503,10 +502,10 @@
mDreamOverlayContainerViewController =
dreamOverlayComponent.getDreamOverlayContainerViewController();
- if (!SceneContainerFlag.isEnabled()) {
- mTouchMonitor = ambientTouchComponent.getTouchMonitor();
- mTouchMonitor.init();
- }
+ // Touch monitor are also used with SceneContainer. See individual touch handlers for
+ // handling of SceneContainer.
+ mTouchMonitor = ambientTouchComponent.getTouchMonitor();
+ mTouchMonitor.init();
mStateController.setShouldShowComplications(shouldShowComplications());
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index 12984efb..85fb90d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -30,8 +30,10 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayContainerView;
import com.android.systemui.res.R;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.touch.TouchInsetManager;
+import dagger.BindsOptionalOf;
import dagger.Module;
import dagger.Provides;
@@ -54,6 +56,13 @@
public static final String DREAM_IN_TRANSLATION_Y_DURATION =
"dream_in_complications_translation_y_duration";
+ /**
+ * Window root view is used to send touches to the scene container. Declaring as optional as it
+ * may not be present on all SysUI variants.
+ */
+ @BindsOptionalOf
+ abstract WindowRootView bindWindowRootView();
+
/** */
@Provides
@DreamOverlayComponent.DreamOverlayScope
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
index 5ba780f..42a6877 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -31,6 +31,9 @@
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
import com.android.systemui.communal.domain.interactor.CommunalInteractor;
import com.android.systemui.dreams.touch.dagger.CommunalTouchModule;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import kotlinx.coroutines.Job;
@@ -42,6 +45,7 @@
import javax.inject.Inject;
import javax.inject.Named;
+import javax.inject.Provider;
/** {@link TouchHandler} responsible for handling touches to open communal hub. **/
public class CommunalTouchHandler implements TouchHandler {
@@ -51,6 +55,8 @@
private final CommunalInteractor mCommunalInteractor;
private final ConfigurationInteractor mConfigurationInteractor;
+ private final SceneInteractor mSceneInteractor;
+ private final WindowRootView mWindowRootView;
private Boolean mIsEnabled = false;
private ArrayList<Job> mFlows = new ArrayList<>();
@@ -69,12 +75,16 @@
@Named(CommunalTouchModule.COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth,
CommunalInteractor communalInteractor,
ConfigurationInteractor configurationInteractor,
+ SceneInteractor sceneInteractor,
+ Optional<Provider<WindowRootView>> windowRootViewProvider,
Lifecycle lifecycle) {
mInitiationWidth = initiationWidth;
mCentralSurfaces = centralSurfaces;
mLifecycle = lifecycle;
mCommunalInteractor = communalInteractor;
mConfigurationInteractor = configurationInteractor;
+ mSceneInteractor = sceneInteractor;
+ mWindowRootView = windowRootViewProvider.get().get();
mFlows.add(collectFlow(
mLifecycle,
@@ -125,8 +135,15 @@
private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) {
// Notification shade window has its own logic to be visible if the hub is open, no need to
// do anything here other than send touch events over.
+ if (SceneContainerFlag.isEnabled()) {
+ mSceneInteractor.onRemoteUserInputStarted("communal touch handler");
+ }
session.registerInputListener(ev -> {
- surfaces.handleCommunalHubTouch((MotionEvent) ev);
+ if (SceneContainerFlag.isEnabled()) {
+ mWindowRootView.dispatchTouchEvent((MotionEvent) ev);
+ } else {
+ surfaces.handleCommunalHubTouch((MotionEvent) ev);
+ }
if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
var unused = session.pop();
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
index 82b4825..2fa3405 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
@@ -32,10 +32,8 @@
* media projection. Null if the media projection is going to this same device (e.g. another
* app is recording the screen).
*/
- sealed class Projecting(
- open val hostPackage: String,
- open val hostDeviceName: String?,
- ) : MediaProjectionState {
+ sealed class Projecting(open val hostPackage: String, open val hostDeviceName: String?) :
+ MediaProjectionState {
/** The entire screen is being projected. */
data class EntireScreen(
override val hostPackage: String,
@@ -48,5 +46,11 @@
override val hostDeviceName: String?,
val task: RunningTaskInfo,
) : Projecting(hostPackage, hostDeviceName)
+
+ /** The screen is not being projected, only audio is being projected. */
+ data class NoScreen(
+ override val hostPackage: String,
+ override val hostDeviceName: String? = null,
+ ) : Projecting(hostPackage, hostDeviceName)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index 5704e80..35efd75 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -23,6 +23,7 @@
import android.os.Handler
import android.view.ContentRecordingSession
import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
+import com.android.systemui.Flags
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -94,7 +95,7 @@
{},
{ "MediaProjectionManager.Callback#onStart" },
)
- trySendWithFailureLogging(CallbackEvent.OnStart, TAG)
+ trySendWithFailureLogging(CallbackEvent.OnStart(info), TAG)
}
override fun onStop(info: MediaProjectionInfo?) {
@@ -109,7 +110,7 @@
override fun onRecordingSessionSet(
info: MediaProjectionInfo,
- session: ContentRecordingSession?
+ session: ContentRecordingSession?,
) {
logger.log(
TAG,
@@ -142,7 +143,21 @@
// #onRecordingSessionSet and we don't emit "Projecting".
.mapLatest {
when (it) {
- is CallbackEvent.OnStart,
+ is CallbackEvent.OnStart -> {
+ if (!Flags.statusBarShowAudioOnlyProjectionChip()) {
+ return@mapLatest MediaProjectionState.NotProjecting
+ }
+ // It's possible for a projection to be audio-only, in which case `OnStart`
+ // will occur but `OnRecordingSessionSet` will not. We should still consider
+ // us to be projecting even if only audio is projecting. See b/373308507.
+ if (it.info != null) {
+ MediaProjectionState.Projecting.NoScreen(
+ hostPackage = it.info.packageName
+ )
+ } else {
+ MediaProjectionState.NotProjecting
+ }
+ }
is CallbackEvent.OnStop -> MediaProjectionState.NotProjecting
is CallbackEvent.OnRecordingSessionSet -> stateForSession(it.info, it.session)
}
@@ -155,7 +170,7 @@
private suspend fun stateForSession(
info: MediaProjectionInfo,
- session: ContentRecordingSession?
+ session: ContentRecordingSession?,
): MediaProjectionState {
if (session == null) {
return MediaProjectionState.NotProjecting
@@ -184,7 +199,7 @@
* the correct callback ordering.
*/
sealed interface CallbackEvent {
- data object OnStart : CallbackEvent
+ data class OnStart(val info: MediaProjectionInfo?) : CallbackEvent
data object OnStop : CallbackEvent
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
index 118639c..ccc54f1 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
@@ -68,6 +68,7 @@
}
}
is MediaProjectionState.Projecting.EntireScreen,
+ is MediaProjectionState.Projecting.NoScreen,
is MediaProjectionState.NotProjecting -> {
flowOf(TaskSwitchState.NotProjectingTask)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt
new file mode 100644
index 0000000..6d8e898
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpandsOnStatusBarLongPress.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the shade expands on status bar long press flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object ShadeExpandsOnStatusBarLongPress {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_SHADE_EXPANDS_ON_STATUS_BAR_LONG_PRESS
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.shadeExpandsOnStatusBarLongPress()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This will throw an exception if
+ * the flag is not enabled to ensure that the refactor author catches issues in testing.
+ * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+ */
+ @JvmStatic
+ inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
index 0e1bf72..5db1dcb 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt
@@ -16,6 +16,7 @@
package com.android.systemui.smartspace.config
+import com.android.systemui.Flags.smartspaceSwipeEventLogging
import com.android.systemui.Flags.smartspaceViewpager2
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.plugins.BcSmartspaceConfigPlugin
@@ -27,4 +28,7 @@
override val isViewPager2Enabled: Boolean
get() = smartspaceViewpager2()
+
+ override val isSwipeEventLoggingEnabled: Boolean
+ get() = smartspaceSwipeEventLogging()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index d4ad6ee..1107206 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -68,23 +68,24 @@
private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
@StatusBarChipsLog private val logger: LogBuffer,
) : OngoingActivityChipViewModel {
- /**
- * The cast chip to show, based only on MediaProjection API events.
- *
- * This chip will only be [OngoingActivityChipModel.Shown] when the user is casting their
- * *screen*. If the user is only casting audio, this chip will be
- * [OngoingActivityChipModel.Hidden].
- */
+ /** The cast chip to show, based only on MediaProjection API events. */
private val projectionChip: StateFlow<OngoingActivityChipModel> =
mediaProjectionChipInteractor.projection
.map { projectionModel ->
when (projectionModel) {
is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
is ProjectionChipModel.Projecting -> {
- if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) {
- OngoingActivityChipModel.Hidden()
- } else {
- createCastScreenToOtherDeviceChip(projectionModel)
+ when (projectionModel.receiver) {
+ ProjectionChipModel.Receiver.CastToOtherDevice -> {
+ when (projectionModel.contentType) {
+ ProjectionChipModel.ContentType.Screen ->
+ createCastScreenToOtherDeviceChip(projectionModel)
+ ProjectionChipModel.ContentType.Audio ->
+ createIconOnlyCastChip(deviceName = null)
+ }
+ }
+ ProjectionChipModel.Receiver.ShareToApp ->
+ OngoingActivityChipModel.Hidden()
}
}
}
@@ -98,9 +99,9 @@
* This chip will be [OngoingActivityChipModel.Shown] when the user is casting their screen *or*
* their audio.
*
- * The MediaProjection APIs are not invoked for casting *only audio* to another device because
- * MediaProjection is only concerned with *screen* sharing (see b/342169876). We listen to
- * MediaRouter APIs here to cover audio-only casting.
+ * The MediaProjection APIs are typically not invoked for casting *only audio* to another device
+ * because MediaProjection is only concerned with *screen* sharing (see b/342169876). We listen
+ * to MediaRouter APIs here to cover audio-only casting.
*
* Note that this means we will start showing the cast chip before the casting actually starts,
* for **both** audio-only casting and screen casting. MediaRouter is aware of all
@@ -139,7 +140,7 @@
str1 = projection.logName
str2 = router.logName
},
- { "projectionChip=$str1 > routerChip=$str2" }
+ { "projectionChip=$str1 > routerChip=$str2" },
)
// A consequence of b/269975671 is that MediaRouter and MediaProjection APIs fire at
@@ -186,7 +187,7 @@
}
private fun createCastScreenToOtherDeviceChip(
- state: ProjectionChipModel.Projecting,
+ state: ProjectionChipModel.Projecting
): OngoingActivityChipModel.Shown {
return OngoingActivityChipModel.Shown.Timer(
icon =
@@ -195,7 +196,7 @@
CAST_TO_OTHER_DEVICE_ICON,
// This string is "Casting screen"
ContentDescription.Resource(
- R.string.cast_screen_to_other_device_chip_accessibility_label,
+ R.string.cast_screen_to_other_device_chip_accessibility_label
),
)
),
@@ -236,9 +237,7 @@
)
}
- private fun createCastScreenToOtherDeviceDialogDelegate(
- state: ProjectionChipModel.Projecting,
- ) =
+ private fun createCastScreenToOtherDeviceDialogDelegate(state: ProjectionChipModel.Projecting) =
EndCastScreenToOtherDeviceDialogDelegate(
endMediaProjectionDialogHelper,
context,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
index 8abe1d3..27b2465 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor
import android.content.pm.PackageManager
+import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
@@ -59,23 +60,43 @@
ProjectionChipModel.NotProjecting
}
is MediaProjectionState.Projecting -> {
- val type =
+ val receiver =
if (packageHasCastingCapabilities(packageManager, state.hostPackage)) {
- ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE
+ ProjectionChipModel.Receiver.CastToOtherDevice
} else {
- ProjectionChipModel.Type.SHARE_TO_APP
+ ProjectionChipModel.Receiver.ShareToApp
}
+ val contentType =
+ if (Flags.statusBarShowAudioOnlyProjectionChip()) {
+ when (state) {
+ is MediaProjectionState.Projecting.EntireScreen,
+ is MediaProjectionState.Projecting.SingleTask ->
+ ProjectionChipModel.ContentType.Screen
+ is MediaProjectionState.Projecting.NoScreen ->
+ ProjectionChipModel.ContentType.Audio
+ }
+ } else {
+ ProjectionChipModel.ContentType.Screen
+ }
+
logger.log(
TAG,
LogLevel.INFO,
{
- str1 = type.name
- str2 = state.hostPackage
- str3 = state.hostDeviceName
+ bool1 = receiver == ProjectionChipModel.Receiver.CastToOtherDevice
+ bool2 = contentType == ProjectionChipModel.ContentType.Screen
+ str1 = state.hostPackage
+ str2 = state.hostDeviceName
},
- { "State: Projecting(type=$str1 hostPackage=$str2 hostDevice=$str3)" }
+ {
+ "State: Projecting(" +
+ "receiver=${if (bool1) "CastToOtherDevice" else "ShareToApp"} " +
+ "contentType=${if (bool2) "Screen" else "Audio"} " +
+ "hostPackage=$str1 " +
+ "hostDevice=$str2)"
+ },
)
- ProjectionChipModel.Projecting(type, state)
+ ProjectionChipModel.Projecting(receiver, contentType, state)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
index 85682f5..c6283e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/ProjectionChipModel.kt
@@ -28,16 +28,22 @@
/** Media is currently being projected. */
data class Projecting(
- val type: Type,
+ val receiver: Receiver,
+ val contentType: ContentType,
val projectionState: MediaProjectionState.Projecting,
) : ProjectionChipModel()
- enum class Type {
- /**
- * This projection is sharing your phone screen content to another app on the same device.
- */
- SHARE_TO_APP,
- /** This projection is sharing your phone screen content to a different device. */
- CAST_TO_OTHER_DEVICE,
+ enum class Receiver {
+ /** This projection is sharing to another app on the same device. */
+ ShareToApp,
+ /** This projection is sharing to a different device. */
+ CastToOtherDevice,
+ }
+
+ enum class ContentType {
+ /** This projection is sharing your device's screen content. */
+ Screen,
+ /** This projection is sharing your device's audio (but *not* screen). */
+ Audio,
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
new file mode 100644
index 0000000..8ec0567
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.sharetoapp.ui.view
+
+import android.content.Context
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
+import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel.Companion.SHARE_TO_APP_ICON
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/**
+ * A dialog that lets the user stop an ongoing share-to-app event. The user could be sharing their
+ * screen or just sharing their audio. This dialog uses generic strings to handle both cases well.
+ */
+class EndGenericShareToAppDialogDelegate(
+ private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
+ private val context: Context,
+ private val stopAction: () -> Unit,
+) : SystemUIDialog.Delegate {
+ override fun createDialog(): SystemUIDialog {
+ return endMediaProjectionDialogHelper.createDialog(this)
+ }
+
+ override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ val message = context.getString(R.string.share_to_app_stop_dialog_message_generic)
+ with(dialog) {
+ setIcon(SHARE_TO_APP_ICON)
+ setTitle(R.string.share_to_app_stop_dialog_title_generic)
+ setMessage(message)
+ // No custom on-click, because the dialog will automatically be dismissed when the
+ // button is clicked anyway.
+ setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
+ setPositiveButton(
+ R.string.share_to_app_stop_dialog_button,
+ endMediaProjectionDialogHelper.wrapStopAction(stopAction),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
index d10bd77..053016e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
@@ -26,7 +26,7 @@
import com.android.systemui.statusbar.phone.SystemUIDialog
/** A dialog that lets the user stop an ongoing share-screen-to-app event. */
-class EndShareToAppDialogDelegate(
+class EndShareScreenToAppDialogDelegate(
private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
private val context: Context,
private val stopAction: () -> Unit,
@@ -71,7 +71,7 @@
if (hostAppName != null) {
context.getString(
R.string.share_to_app_stop_dialog_message_entire_screen_with_host_app,
- hostAppName
+ hostAppName,
)
} else {
context.getString(R.string.share_to_app_stop_dialog_message_entire_screen)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index d99a916..11d077f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -32,7 +32,8 @@
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
-import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareScreenToAppDialogDelegate
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
@@ -68,10 +69,17 @@
when (projectionModel) {
is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden()
is ProjectionChipModel.Projecting -> {
- if (projectionModel.type != ProjectionChipModel.Type.SHARE_TO_APP) {
- OngoingActivityChipModel.Hidden()
- } else {
- createShareToAppChip(projectionModel)
+ when (projectionModel.receiver) {
+ ProjectionChipModel.Receiver.ShareToApp -> {
+ when (projectionModel.contentType) {
+ ProjectionChipModel.ContentType.Screen ->
+ createShareScreenToAppChip(projectionModel)
+ ProjectionChipModel.ContentType.Audio ->
+ createIconOnlyShareToAppChip()
+ }
+ }
+ ProjectionChipModel.Receiver.CastToOtherDevice ->
+ OngoingActivityChipModel.Hidden()
}
}
}
@@ -105,8 +113,8 @@
mediaProjectionChipInteractor.stopProjecting()
}
- private fun createShareToAppChip(
- state: ProjectionChipModel.Projecting,
+ private fun createShareScreenToAppChip(
+ state: ProjectionChipModel.Projecting
): OngoingActivityChipModel.Shown {
return OngoingActivityChipModel.Shown.Timer(
icon =
@@ -120,11 +128,33 @@
// TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
startTimeMs = systemClock.elapsedRealtime(),
createDialogLaunchOnClickListener(
- createShareToAppDialogDelegate(state),
+ createShareScreenToAppDialogDelegate(state),
+ dialogTransitionAnimator,
+ DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Share to app"),
+ logger,
+ TAG,
+ ),
+ )
+ }
+
+ private fun createIconOnlyShareToAppChip(): OngoingActivityChipModel.Shown {
+ return OngoingActivityChipModel.Shown.IconOnly(
+ icon =
+ OngoingActivityChipModel.ChipIcon.SingleColorIcon(
+ Icon.Resource(
+ SHARE_TO_APP_ICON,
+ ContentDescription.Resource(
+ R.string.share_to_app_chip_accessibility_label_generic
+ ),
+ )
+ ),
+ colors = ColorsModel.Red,
+ createDialogLaunchOnClickListener(
+ createGenericShareToAppDialogDelegate(),
dialogTransitionAnimator,
DialogCuj(
Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP,
- tag = "Share to app",
+ tag = "Share to app audio only",
),
logger,
TAG,
@@ -132,14 +162,21 @@
)
}
- private fun createShareToAppDialogDelegate(state: ProjectionChipModel.Projecting) =
- EndShareToAppDialogDelegate(
+ private fun createShareScreenToAppDialogDelegate(state: ProjectionChipModel.Projecting) =
+ EndShareScreenToAppDialogDelegate(
endMediaProjectionDialogHelper,
context,
stopAction = this::stopProjectingFromDialog,
state,
)
+ private fun createGenericShareToAppDialogDelegate() =
+ EndGenericShareToAppDialogDelegate(
+ endMediaProjectionDialogHelper,
+ context,
+ stopAction = this::stopProjectingFromDialog,
+ )
+
companion object {
@DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all
private const val TAG = "ShareToAppVM"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
index 525f3de..72cd63f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.dagger;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.emergency.EmergencyGestureModule;
import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
import com.android.systemui.statusbar.notification.row.NotificationRowModule;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -32,7 +31,7 @@
*/
@Module(includes = {CentralSurfacesDependenciesModule.class,
StatusBarNotificationPresenterModule.class,
- NotificationsModule.class, NotificationRowModule.class, EmergencyGestureModule.class})
+ NotificationsModule.class, NotificationRowModule.class})
public interface CentralSurfacesModule {
/**
* Provides our instance of CentralSurfaces which is considered optional.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index e5ce25d..bf30322 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -309,7 +309,20 @@
}
}
- /** Set onClickListener for the manage/history button. */
+ /** Set onClickListener for the notification settings button. */
+ public void setSettingsButtonClickListener(OnClickListener listener) {
+ mSettingsButton.setOnClickListener(listener);
+ }
+
+ /** Set onClickListener for the notification history button. */
+ public void setHistoryButtonClickListener(OnClickListener listener) {
+ mHistoryButton.setOnClickListener(listener);
+ }
+
+ /**
+ * Set onClickListener for the manage/history button. This is replaced by two separate buttons
+ * in the redesign.
+ */
public void setManageButtonClickListener(OnClickListener listener) {
mManageOrHistoryButton.setOnClickListener(listener);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index ddd9cdd..34894a2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -18,9 +18,12 @@
import android.view.View
import androidx.lifecycle.lifecycleScope
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.Flags
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.notification.NotificationActivityStarter
+import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
@@ -29,7 +32,6 @@
import com.android.systemui.util.ui.value
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.coroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Binds a [FooterView] to its [view model][FooterViewModel]. */
object FooterViewBinder {
@@ -74,6 +76,9 @@
notificationActivityStarter,
)
}
+ } else {
+ bindSettingsButtonListener(footer, notificationActivityStarter)
+ bindHistoryButtonListener(footer, notificationActivityStarter)
}
launch { bindMessage(footer, viewModel) }
}
@@ -117,6 +122,34 @@
}
}
+ private fun bindSettingsButtonListener(
+ footer: FooterView,
+ notificationActivityStarter: NotificationActivityStarter,
+ ) {
+ val settingsIntent =
+ SettingsIntent.forNotificationSettings(
+ cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+ )
+ val onClickListener = { view: View ->
+ notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+ }
+ footer.setSettingsButtonClickListener(onClickListener)
+ }
+
+ private fun bindHistoryButtonListener(
+ footer: FooterView,
+ notificationActivityStarter: NotificationActivityStarter,
+ ) {
+ val settingsIntent =
+ SettingsIntent.forNotificationHistory(
+ cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+ )
+ val onClickListener = { view: View ->
+ notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+ }
+ footer.setHistoryButtonClickListener(onClickListener)
+ }
+
private suspend fun bindManageOrHistoryButton(
footer: FooterView,
viewModel: FooterViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index a3f4cd2..d8021fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -98,7 +98,6 @@
val manageButtonShouldLaunchHistory =
notificationSettingsInteractor.isNotificationHistoryEnabled
- // TODO(b/366003631): When inlining the flag, consider adding this to FooterButtonViewModel.
val manageOrHistoryButtonClick: Flow<SettingsIntent> by lazy {
if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
flowOf(SettingsIntent(Intent(Settings.ACTION_NOTIFICATION_SETTINGS)))
@@ -124,7 +123,11 @@
else R.string.manage_notifications_text
}
- /** The button for managing notification settings or opening notification history. */
+ /**
+ * The button for managing notification settings or opening notification history. This is
+ * replaced by two separate buttons in the redesign. These are currently static, and therefore
+ * not modeled here, but if that changes we can also add them as FooterButtonViewModels.
+ */
val manageOrHistoryButton: FooterButtonViewModel =
FooterButtonViewModel(
labelId = manageOrHistoryButtonText,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index dde83b9..c1d72e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -6953,12 +6953,10 @@
/** Use {@link ScrollViewFields#intrinsicStackHeight}, when SceneContainerFlag is enabled. */
private int getContentHeight() {
- SceneContainerFlag.assertInLegacyMode();
return mContentHeight;
}
private void setContentHeight(int contentHeight) {
- SceneContainerFlag.assertInLegacyMode();
mContentHeight = contentHeight;
}
@@ -6967,12 +6965,10 @@
* @return the height of the content ignoring the footer.
*/
public float getIntrinsicContentHeight() {
- SceneContainerFlag.assertInLegacyMode();
return mIntrinsicContentHeight;
}
private void setIntrinsicContentHeight(float intrinsicContentHeight) {
- SceneContainerFlag.assertInLegacyMode();
mIntrinsicContentHeight = intrinsicContentHeight;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
index 7265b821..281e57f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractor.kt
@@ -21,6 +21,7 @@
import android.media.AudioManager.RINGER_MODE_SILENT
import android.media.AudioManager.RINGER_MODE_VIBRATE
import android.provider.Settings
+import com.android.settingslib.volume.data.repository.AudioSystemRepository
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.plugins.VolumeDialogController
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
@@ -43,6 +44,7 @@
@VolumeDialog private val coroutineScope: CoroutineScope,
volumeDialogStateInteractor: VolumeDialogStateInteractor,
private val controller: VolumeDialogController,
+ private val audioSystemRepository: AudioSystemRepository,
) {
val ringerModel: Flow<VolumeDialogRingerModel> =
@@ -70,6 +72,7 @@
isMuted = it.level == 0 || it.muted,
level = it.level,
levelMax = it.levelMax,
+ isSingleVolume = audioSystemRepository.isSingleVolume,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
index cf23f1a..3c24e02 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/shared/model/VolumeDialogRingerModel.kt
@@ -31,4 +31,6 @@
val level: Int,
/** Ring stream maximum level */
val levelMax: Int,
+ /** in single volume mode */
+ val isSingleVolume: Boolean,
)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModelState.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModelState.kt
new file mode 100644
index 0000000..78b00af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModelState.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ringer.ui.viewmodel
+
+/** Models ringer view model state. */
+sealed class RingerViewModelState {
+
+ data class Available(val uiModel: RingerViewModel) : RingerViewModelState()
+
+ data object Unavailable : RingerViewModelState()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
index ac82ae3..5b73107 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
@@ -34,11 +34,10 @@
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
@@ -56,13 +55,12 @@
private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)
- val ringerViewModel: Flow<RingerViewModel> =
+ val ringerViewModel: StateFlow<RingerViewModelState> =
combine(interactor.ringerModel, drawerState) { ringerModel, state ->
ringerModel.toViewModel(state)
}
.flowOn(backgroundDispatcher)
- .stateIn(coroutineScope, SharingStarted.Eagerly, null)
- .filterNotNull()
+ .stateIn(coroutineScope, SharingStarted.Eagerly, RingerViewModelState.Unavailable)
// Vibration attributes.
private val sonificiationVibrationAttributes =
@@ -105,16 +103,22 @@
private fun VolumeDialogRingerModel.toViewModel(
drawerState: RingerDrawerState
- ): RingerViewModel {
+ ): RingerViewModelState {
val currentIndex = availableModes.indexOf(currentRingerMode)
if (currentIndex == -1) {
volumeDialogLogger.onCurrentRingerModeIsUnsupported(currentRingerMode)
}
- return RingerViewModel(
- availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
- currentButtonIndex = currentIndex,
- drawerState = drawerState,
- )
+ return if (currentIndex == -1 || isSingleVolume) {
+ RingerViewModelState.Unavailable
+ } else {
+ RingerViewModelState.Available(
+ RingerViewModel(
+ availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
+ currentButtonIndex = currentIndex,
+ drawerState = drawerState,
+ )
+ )
+ }
}
private fun VolumeDialogRingerModel.toButtonViewModel(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryImplTest.kt
index 5d5c120..da7a723 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryImplTest.kt
@@ -23,7 +23,7 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -38,7 +38,7 @@
@SmallTest
class DisplayScopeRepositoryImplTest : SysuiTestCase() {
- private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
private val fakeDisplayRepository = kosmos.displayRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
index ff3186a..5a76489 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
@@ -25,9 +25,8 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.shared.model.DisplayWindowProperties
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -44,7 +43,7 @@
@SmallTest
class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() {
- private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val fakeDisplayRepository = kosmos.displayRepository
private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index fa69fdd..929b0aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -50,8 +50,9 @@
import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRendererFactory
import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
-import com.android.systemui.kosmos.unconfinedTestDispatcher
-import com.android.systemui.kosmos.unconfinedTestScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -67,7 +68,6 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.fakeSettings
-import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -89,10 +89,10 @@
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class CustomizationProviderTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testDispatcher = kosmos.unconfinedTestDispatcher
- private val testScope = kosmos.unconfinedTestScope
- private val fakeSettings = kosmos.unconfinedDispatcherFakeSettings
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testDispatcher = kosmos.testDispatcher
+ private val testScope = kosmos.testScope
+ private val fakeSettings = kosmos.fakeSettings
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var keyguardStateController: KeyguardStateController
@@ -129,13 +129,7 @@
context = context,
userFileManager =
mock<UserFileManager>().apply {
- whenever(
- getSharedPreferences(
- anyString(),
- anyInt(),
- anyInt(),
- )
- )
+ whenever(getSharedPreferences(anyString(), anyInt(), anyInt()))
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
@@ -288,10 +282,7 @@
val affordanceId = AFFORDANCE_2
val affordanceName = AFFORDANCE_2_NAME
- insertSelection(
- slotId = slotId,
- affordanceId = affordanceId,
- )
+ insertSelection(slotId = slotId, affordanceId = affordanceId)
assertThat(querySelections())
.isEqualTo(
@@ -311,14 +302,8 @@
assertThat(querySlots())
.isEqualTo(
listOf(
- Slot(
- id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
- capacity = 1,
- ),
- Slot(
- id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
- capacity = 1,
- ),
+ Slot(id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, capacity = 1),
+ Slot(id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, capacity = 1),
)
)
runCurrent()
@@ -330,16 +315,8 @@
assertThat(queryAffordances())
.isEqualTo(
listOf(
- Affordance(
- id = AFFORDANCE_1,
- name = AFFORDANCE_1_NAME,
- iconResourceId = 1,
- ),
- Affordance(
- id = AFFORDANCE_2,
- name = AFFORDANCE_2_NAME,
- iconResourceId = 2,
- ),
+ Affordance(id = AFFORDANCE_1, name = AFFORDANCE_1_NAME, iconResourceId = 1),
+ Affordance(id = AFFORDANCE_2, name = AFFORDANCE_2_NAME, iconResourceId = 2),
)
)
}
@@ -361,10 +338,7 @@
"${Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID} = ? AND" +
" ${Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID}" +
" = ?",
- arrayOf(
- KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
- AFFORDANCE_2,
- ),
+ arrayOf(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, AFFORDANCE_2),
)
assertThat(querySelections())
@@ -394,9 +368,7 @@
context.contentResolver.delete(
Contract.LockScreenQuickAffordances.SelectionTable.URI,
Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID,
- arrayOf(
- KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
- ),
+ arrayOf(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END),
)
assertThat(querySelections())
@@ -428,31 +400,22 @@
assertThat(result.containsKey(KeyguardRemotePreviewManager.KEY_PREVIEW_CALLBACK))
}
- private fun insertSelection(
- slotId: String,
- affordanceId: String,
- ) {
+ private fun insertSelection(slotId: String, affordanceId: String) {
context.contentResolver.insert(
Contract.LockScreenQuickAffordances.SelectionTable.URI,
ContentValues().apply {
put(Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID, slotId)
put(
Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID,
- affordanceId
+ affordanceId,
)
- }
+ },
)
}
private fun querySelections(): List<Selection> {
return context.contentResolver
- .query(
- Contract.LockScreenQuickAffordances.SelectionTable.URI,
- null,
- null,
- null,
- null,
- )
+ .query(Contract.LockScreenQuickAffordances.SelectionTable.URI, null, null, null, null)
?.use { cursor ->
buildList {
val slotIdColumnIndex =
@@ -491,13 +454,7 @@
private fun querySlots(): List<Slot> {
return context.contentResolver
- .query(
- Contract.LockScreenQuickAffordances.SlotTable.URI,
- null,
- null,
- null,
- null,
- )
+ .query(Contract.LockScreenQuickAffordances.SlotTable.URI, null, null, null, null)
?.use { cursor ->
buildList {
val idColumnIndex =
@@ -526,13 +483,7 @@
private fun queryAffordances(): List<Affordance> {
return context.contentResolver
- .query(
- Contract.LockScreenQuickAffordances.AffordanceTable.URI,
- null,
- null,
- null,
- null,
- )
+ .query(Contract.LockScreenQuickAffordances.AffordanceTable.URI, null, null, null, null)
?.use { cursor ->
buildList {
val idColumnIndex =
@@ -564,22 +515,11 @@
} ?: emptyList()
}
- data class Slot(
- val id: String,
- val capacity: Int,
- )
+ data class Slot(val id: String, val capacity: Int)
- data class Affordance(
- val id: String,
- val name: String,
- val iconResourceId: Int,
- )
+ data class Affordance(val id: String, val name: String, val iconResourceId: Int)
- data class Selection(
- val slotId: String,
- val affordanceId: String,
- val affordanceName: String,
- )
+ data class Selection(val slotId: String, val affordanceId: String, val affordanceName: String)
companion object {
private const val AFFORDANCE_1 = "affordance_1"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 570c640..33b61a09 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -48,7 +48,7 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
@@ -73,7 +73,7 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.settings.GlobalSettings
-import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings
+import com.android.systemui.util.settings.fakeSettings
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Locale
@@ -119,9 +119,9 @@
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(ParameterizedAndroidJunit4::class)
class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testDispatcher = kosmos.unconfinedTestDispatcher
- private val secureSettings = kosmos.unconfinedDispatcherFakeSettings
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testDispatcher = kosmos.testDispatcher
+ private val secureSettings = kosmos.fakeSettings
@Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
@Mock lateinit var mediaViewControllerFactory: Provider<MediaViewController>
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index f8df707..a9a80b5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -8,9 +8,26 @@
import kotlinx.coroutines.test.UnconfinedTestDispatcher
var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() }
-var Kosmos.unconfinedTestDispatcher by Fixture { UnconfinedTestDispatcher() }
+
+/**
+ * Force this Kosmos to use a [StandardTestDispatcher], regardless of the current Kosmos default. In
+ * short, no launch blocks will be run on this dispatcher until `TestCoroutineScheduler.runCurrent`
+ * is called. See [StandardTestDispatcher] for details.
+ *
+ * For details on this migration, see http://go/thetiger
+ */
+fun Kosmos.useStandardTestDispatcher() = apply { testDispatcher = StandardTestDispatcher() }
+
+/**
+ * Force this Kosmos to use an [UnconfinedTestDispatcher], regardless of the current Kosmos default.
+ * In short, launch blocks will be executed eagerly without waiting for
+ * `TestCoroutineScheduler.runCurrent`. See [UnconfinedTestDispatcher] for details.
+ *
+ * For details on this migration, see http://go/thetiger
+ */
+fun Kosmos.useUnconfinedTestDispatcher() = apply { testDispatcher = UnconfinedTestDispatcher() }
+
var Kosmos.testScope by Fixture { TestScope(testDispatcher) }
-var Kosmos.unconfinedTestScope by Fixture { TestScope(unconfinedTestDispatcher) }
var Kosmos.applicationCoroutineScope by Fixture { testScope.backgroundScope }
var Kosmos.testCase: SysuiTestCase by Fixture()
var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
index 73d423c..35fa2af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt
@@ -19,10 +19,5 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.unconfinedTestDispatcher
val Kosmos.fakeGlobalSettings: FakeGlobalSettings by Fixture { FakeGlobalSettings(testDispatcher) }
-
-val Kosmos.unconfinedDispatcherFakeGlobalSettings: FakeGlobalSettings by Fixture {
- FakeGlobalSettings(unconfinedTestDispatcher)
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
index e1daf9b..76ef202 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt
@@ -19,13 +19,8 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.unconfinedTestDispatcher
import com.android.systemui.settings.userTracker
val Kosmos.fakeSettings: FakeSettings by Fixture {
FakeSettings(testDispatcher) { userTracker.userId }
}
-
-val Kosmos.unconfinedDispatcherFakeSettings: FakeSettings by Fixture {
- FakeSettings(unconfinedTestDispatcher) { userTracker.userId }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
index c2a1544..1addd91 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt
@@ -19,6 +19,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.volume.data.repository.audioSystemRepository
import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor
val Kosmos.volumeDialogRingerInteractor by
@@ -27,5 +28,6 @@
coroutineScope = applicationCoroutineScope,
volumeDialogStateInteractor = volumeDialogStateInteractor,
controller = volumeDialogController,
+ audioSystemRepository = audioSystemRepository,
)
}
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index f6ac706..35998d9a 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -17,6 +17,7 @@
package com.android.server.appwidget;
import static android.appwidget.flags.Flags.remoteAdapterConversion;
+import static android.appwidget.flags.Flags.remoteViewsProto;
import static android.appwidget.flags.Flags.removeAppWidgetServiceIoFromCriticalPath;
import static android.appwidget.flags.Flags.securityPolicyInteractAcrossUsers;
import static android.appwidget.flags.Flags.supportResumeRestoreAfterReboot;
@@ -31,6 +32,7 @@
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.PermissionName;
@@ -104,6 +106,7 @@
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.service.appwidget.AppWidgetServiceDumpProto;
+import android.service.appwidget.GeneratedPreviewsProto;
import android.service.appwidget.WidgetProto;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -122,7 +125,9 @@
import android.util.SparseLongArray;
import android.util.TypedValue;
import android.util.Xml;
+import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
import android.view.Display;
import android.view.View;
import android.widget.RemoteViews;
@@ -134,6 +139,7 @@
import com.android.internal.appwidget.IAppWidgetHost;
import com.android.internal.appwidget.IAppWidgetService;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.infra.AndroidFuture;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
@@ -221,6 +227,10 @@
// XML attribute for widget ids that are pending deletion.
// See {@link Provider#pendingDeletedWidgetIds}.
private static final String PENDING_DELETED_IDS_ATTR = "pending_deleted_ids";
+ // Name of service directory in /data/system_ce/<user>/
+ private static final String APPWIDGET_CE_DATA_DIRNAME = "appwidget";
+ // Name of previews directory in /data/system_ce/<user>/appwidget/
+ private static final String WIDGET_PREVIEWS_DIRNAME = "previews";
// Hard limit of number of hosts an app can create, note that the app that hosts the widgets
// can have multiple instances of {@link AppWidgetHost}, typically in respect to different
@@ -316,6 +326,9 @@
// Handler to the background thread that saves states to disk.
private Handler mSaveStateHandler;
+ // Handler to the background thread that saves generated previews to disk. All operations that
+ // modify saved previews must be run on this Handler.
+ private Handler mSavePreviewsHandler;
// Handler to the foreground thread that handles broadcasts related to user
// and package events, as well as various internal events within
// AppWidgetService.
@@ -359,6 +372,7 @@
} else {
mSaveStateHandler = BackgroundThread.getHandler();
}
+ mSavePreviewsHandler = new Handler(BackgroundThread.get().getLooper());
final ServiceThread serviceThread = new ServiceThread(TAG,
android.os.Process.THREAD_PRIORITY_FOREGROUND, false /* allowIo */);
serviceThread.start();
@@ -378,7 +392,9 @@
SystemUiDeviceConfigFlags.GENERATED_PREVIEW_API_MAX_PROVIDERS,
DEFAULT_GENERATED_PREVIEW_MAX_PROVIDERS);
mGeneratedPreviewsApiCounter = new ApiCounter(generatedPreviewResetInterval,
- generatedPreviewMaxCallsPerInterval, generatedPreviewsMaxProviders);
+ generatedPreviewMaxCallsPerInterval,
+ // Set a limit on the number of providers if storing them in memory.
+ remoteViewsProto() ? Integer.MAX_VALUE : generatedPreviewsMaxProviders);
DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI,
new HandlerExecutor(mCallbackHandler), this::handleSystemUiDeviceConfigChange);
@@ -644,7 +660,14 @@
for (int i = 0; i < providerCount; i++) {
Provider provider = mProviders.get(i);
if (provider.id.uid == clearedUid) {
- changed |= provider.clearGeneratedPreviewsLocked();
+ if (remoteViewsProto()) {
+ changed |= clearGeneratedPreviewsAsync(provider);
+ } else {
+ changed |= provider.clearGeneratedPreviewsLocked();
+ }
+ if (DEBUG) {
+ Slog.e(TAG, "clearPreviewsForUidLocked " + provider + " changed " + changed);
+ }
}
}
return changed;
@@ -3246,6 +3269,9 @@
deleteWidgetsLocked(provider, UserHandle.USER_ALL);
mProviders.remove(provider);
mGeneratedPreviewsApiCounter.remove(provider.id);
+ if (remoteViewsProto()) {
+ clearGeneratedPreviewsAsync(provider);
+ }
// no need to send the DISABLE broadcast, since the receiver is gone anyway
cancelBroadcastsLocked(provider);
@@ -3824,6 +3850,14 @@
} catch (IOException e) {
Slog.w(TAG, "Failed to read state: " + e);
}
+
+ if (remoteViewsProto()) {
+ try {
+ loadGeneratedPreviewCategoriesLocked(profileId);
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to read preview categories: " + e);
+ }
+ }
}
if (version >= 0) {
@@ -4593,6 +4627,12 @@
keep.add(providerId);
// Use the new AppWidgetProviderInfo.
provider.setPartialInfoLocked(info);
+ // Clear old previews
+ if (remoteViewsProto()) {
+ clearGeneratedPreviewsAsync(provider);
+ } else {
+ provider.clearGeneratedPreviewsLocked();
+ }
// If it's enabled
final int M = provider.widgets.size();
if (M > 0) {
@@ -4884,6 +4924,7 @@
mSecurityPolicy.enforceCallFromPackage(callingPackage);
ensureWidgetCategoryCombinationIsValid(widgetCategory);
+ AndroidFuture<RemoteViews> result = null;
synchronized (mLock) {
ensureGroupStateLoadedLocked(profileId);
final int providerCount = mProviders.size();
@@ -4917,10 +4958,23 @@
callingPackage);
if (providerIsInCallerProfile && !shouldFilterAppAccess
&& (providerIsInCallerPackage || hasBindAppWidgetPermission)) {
- return provider.getGeneratedPreviewLocked(widgetCategory);
+ if (remoteViewsProto()) {
+ result = getGeneratedPreviewsAsync(provider, widgetCategory);
+ } else {
+ return provider.getGeneratedPreviewLocked(widgetCategory);
+ }
}
}
}
+
+ if (result != null) {
+ try {
+ return result.get();
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to get generated previews Future result", e);
+ return null;
+ }
+ }
// Either the provider does not exist or the caller does not have permission to access its
// previews.
return null;
@@ -4950,8 +5004,12 @@
providerComponent + " is not a valid AppWidget provider");
}
if (mGeneratedPreviewsApiCounter.tryApiCall(providerId)) {
- provider.setGeneratedPreviewLocked(widgetCategories, preview);
- scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ if (remoteViewsProto()) {
+ setGeneratedPreviewsAsync(provider, widgetCategories, preview);
+ } else {
+ provider.setGeneratedPreviewLocked(widgetCategories, preview);
+ scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ }
return true;
}
return false;
@@ -4979,11 +5037,361 @@
throw new IllegalArgumentException(
providerComponent + " is not a valid AppWidget provider");
}
- final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
- if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+
+ if (remoteViewsProto()) {
+ removeGeneratedPreviewsAsync(provider, widgetCategories);
+ } else {
+ final boolean changed = provider.removeGeneratedPreviewLocked(widgetCategories);
+ if (changed) scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ }
}
}
+ /**
+ * Return previews for the specified provider from a background thread. The result of the future
+ * is nullable.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @NonNull
+ private AndroidFuture<RemoteViews> getGeneratedPreviewsAsync(
+ @NonNull Provider provider, @AppWidgetProviderInfo.CategoryFlags int widgetCategory) {
+ AndroidFuture<RemoteViews> result = new AndroidFuture<>();
+ mSavePreviewsHandler.post(() -> {
+ SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+ for (int i = 0; i < previews.size(); i++) {
+ if ((widgetCategory & previews.keyAt(i)) != 0) {
+ result.complete(previews.valueAt(i));
+ return;
+ }
+ }
+ result.complete(null);
+ });
+ return result;
+ }
+
+ /**
+ * Set previews for the specified provider on a background thread.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private void setGeneratedPreviewsAsync(@NonNull Provider provider, int widgetCategories,
+ @NonNull RemoteViews preview) {
+ mSavePreviewsHandler.post(() -> {
+ SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+ for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+ if ((widgetCategories & flag) != 0) {
+ previews.put(flag, preview);
+ }
+ }
+ saveGeneratedPreviews(provider, previews, /* notify= */ true);
+ });
+ }
+
+ /**
+ * Remove previews for the specified provider on a background thread.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private void removeGeneratedPreviewsAsync(@NonNull Provider provider, int widgetCategories) {
+ mSavePreviewsHandler.post(() -> {
+ SparseArray<RemoteViews> previews = loadGeneratedPreviews(provider);
+ boolean changed = false;
+ for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+ if ((widgetCategories & flag) != 0) {
+ changed |= previews.removeReturnOld(flag) != null;
+ }
+ }
+ if (changed) {
+ saveGeneratedPreviews(provider, previews, /* notify= */ true);
+ }
+ });
+ }
+
+ /**
+ * Clear previews for the specified provider on a background thread. Returns true if changed
+ * (i.e. there are previews to clear). If returns true, the caller should schedule a providers
+ * changed notification.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private boolean clearGeneratedPreviewsAsync(@NonNull Provider provider) {
+ mSavePreviewsHandler.post(() -> {
+ saveGeneratedPreviews(provider, /* previews= */ null, /* notify= */ false);
+ });
+ return provider.info.generatedPreviewCategories != 0;
+ }
+
+ private void checkSavePreviewsThread() {
+ if (DEBUG && !mSavePreviewsHandler.getLooper().isCurrentThread()) {
+ throw new IllegalStateException("Only modify previews on the background thread");
+ }
+ }
+
+ /**
+ * Load previews from file for the given provider. If there are no previews, returns an empty
+ * SparseArray. Else, returns a SparseArray of the previews mapped by widget category.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @NonNull
+ private SparseArray<RemoteViews> loadGeneratedPreviews(@NonNull Provider provider) {
+ checkSavePreviewsThread();
+ try {
+ AtomicFile previewsFile = getWidgetPreviewsFile(provider);
+ if (!previewsFile.exists()) {
+ return new SparseArray<>();
+ }
+ ProtoInputStream input = new ProtoInputStream(previewsFile.readFully());
+ SparseArray<RemoteViews> entries = readGeneratedPreviewsFromProto(input);
+ SparseArray<RemoteViews> singleCategoryKeyedEntries = new SparseArray<>();
+ for (int i = 0; i < entries.size(); i++) {
+ int widgetCategories = entries.keyAt(i);
+ RemoteViews preview = entries.valueAt(i);
+ for (int flag : Provider.WIDGET_CATEGORY_FLAGS) {
+ if ((widgetCategories & flag) != 0) {
+ singleCategoryKeyedEntries.put(flag, preview);
+ }
+ }
+ }
+ return singleCategoryKeyedEntries;
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to load generated previews for " + provider, e);
+ return new SparseArray<>();
+ }
+ }
+
+ /**
+ * This is called when loading profile/group state to populate
+ * AppWidgetProviderInfo.generatedPreviewCategories based on what previews are saved.
+ *
+ * This is the only time previews are read while not on mSavePreviewsHandler. It happens once
+ * per profile during initialization, before any calls to get/set/removeWidgetPreviewAsync
+ * happen for that profile.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @GuardedBy("mLock")
+ private void loadGeneratedPreviewCategoriesLocked(int profileId) throws IOException {
+ for (Provider provider : mProviders) {
+ if (provider.id.getProfile().getIdentifier() != profileId) {
+ continue;
+ }
+ AtomicFile previewsFile = getWidgetPreviewsFile(provider);
+ if (!previewsFile.exists()) {
+ continue;
+ }
+ ProtoInputStream input = new ProtoInputStream(previewsFile.readFully());
+ provider.info.generatedPreviewCategories = readGeneratedPreviewCategoriesFromProto(
+ input);
+ if (DEBUG) {
+ Slog.i(TAG, TextUtils.formatSimple(
+ "loadGeneratedPreviewCategoriesLocked %d %s categories %d", profileId,
+ provider, provider.info.generatedPreviewCategories));
+ }
+ }
+ }
+
+ /**
+ * Save the given previews into storage.
+ *
+ * @param provider Provider for which to save previews
+ * @param previews Previews to save. If null or empty, clears any saved previews for this
+ * provider.
+ * @param notify If true, then this function will notify hosts of updated provider info.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private void saveGeneratedPreviews(@NonNull Provider provider,
+ @Nullable SparseArray<RemoteViews> previews, boolean notify) {
+ checkSavePreviewsThread();
+ AtomicFile file = null;
+ FileOutputStream stream = null;
+ try {
+ file = getWidgetPreviewsFile(provider);
+ if (previews == null || previews.size() == 0) {
+ if (file.exists()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Deleting widget preview file " + file);
+ }
+ file.delete();
+ }
+ } else {
+ if (DEBUG) {
+ Slog.i(TAG, "Writing widget preview file " + file);
+ }
+ ProtoOutputStream out = new ProtoOutputStream();
+ writePreviewsToProto(out, previews);
+ stream = file.startWrite();
+ stream.write(out.getBytes());
+ file.finishWrite(stream);
+ }
+
+ synchronized (mLock) {
+ provider.updateGeneratedPreviewCategoriesLocked(previews);
+ if (notify) {
+ scheduleNotifyGroupHostsForProvidersChangedLocked(provider.getUserId());
+ }
+ }
+ } catch (IOException e) {
+ if (file != null && stream != null) {
+ file.failWrite(stream);
+ }
+ Slog.w(TAG, "Failed to save widget previews for provider " + provider.id.componentName);
+ }
+ }
+
+
+ /**
+ * Write the given previews as a GeneratedPreviewsProto to the output stream.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ private void writePreviewsToProto(@NonNull ProtoOutputStream out,
+ @NonNull SparseArray<RemoteViews> generatedPreviews) {
+ // Collect RemoteViews mapped by hashCode in order to avoid writing duplicates.
+ SparseArray<Pair<Integer, RemoteViews>> previewsToWrite = new SparseArray<>();
+ for (int i = 0; i < generatedPreviews.size(); i++) {
+ int widgetCategory = generatedPreviews.keyAt(i);
+ RemoteViews views = generatedPreviews.valueAt(i);
+ if (!previewsToWrite.contains(views.hashCode())) {
+ previewsToWrite.put(views.hashCode(), new Pair<>(widgetCategory, views));
+ } else {
+ Pair<Integer, RemoteViews> entry = previewsToWrite.get(views.hashCode());
+ previewsToWrite.put(views.hashCode(),
+ Pair.create(entry.first | widgetCategory, views));
+ }
+ }
+
+ for (int i = 0; i < previewsToWrite.size(); i++) {
+ final long token = out.start(GeneratedPreviewsProto.PREVIEWS);
+ Pair<Integer, RemoteViews> entry = previewsToWrite.valueAt(i);
+ out.write(GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES, entry.first);
+ final long viewsToken = out.start(GeneratedPreviewsProto.Preview.VIEWS);
+ entry.second.writePreviewToProto(mContext, out);
+ out.end(viewsToken);
+ out.end(token);
+ }
+ }
+
+ /**
+ * Read a GeneratedPreviewsProto message from the input stream.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @NonNull
+ private SparseArray<RemoteViews> readGeneratedPreviewsFromProto(@NonNull ProtoInputStream input)
+ throws IOException {
+ SparseArray<RemoteViews> entries = new SparseArray<>();
+ while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (input.getFieldNumber()) {
+ case (int) GeneratedPreviewsProto.PREVIEWS:
+ final long token = input.start(GeneratedPreviewsProto.PREVIEWS);
+ Pair<Integer, RemoteViews> entry = readSinglePreviewFromProto(input,
+ /* skipViews= */ false);
+ entries.put(entry.first, entry.second);
+ input.end(token);
+ break;
+ default:
+ Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+ + ProtoUtils.currentFieldToString(input));
+ }
+ }
+ return entries;
+ }
+
+ /**
+ * Read the widget categories from GeneratedPreviewsProto and return an int representing the
+ * combined widget categories of all the previews.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @AppWidgetProviderInfo.CategoryFlags
+ private int readGeneratedPreviewCategoriesFromProto(@NonNull ProtoInputStream input)
+ throws IOException {
+ int widgetCategories = 0;
+ while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (input.getFieldNumber()) {
+ case (int) GeneratedPreviewsProto.PREVIEWS:
+ final long token = input.start(GeneratedPreviewsProto.PREVIEWS);
+ Pair<Integer, RemoteViews> entry = readSinglePreviewFromProto(input,
+ /* skipViews= */ true);
+ widgetCategories |= entry.first;
+ input.end(token);
+ break;
+ default:
+ Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+ + ProtoUtils.currentFieldToString(input));
+ }
+ }
+ return widgetCategories;
+ }
+
+ /**
+ * Read a single GeneratedPreviewsProto.Preview message from the input stream, and returns a
+ * pair of widget category and corresponding RemoteViews. If skipViews is true, this function
+ * will only read widget categories and the returned RemoteViews will be null.
+ */
+ @FlaggedApi(android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO)
+ @NonNull
+ private Pair<Integer, RemoteViews> readSinglePreviewFromProto(@NonNull ProtoInputStream input,
+ boolean skipViews) throws IOException {
+ int widgetCategories = 0;
+ RemoteViews views = null;
+ while (input.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (input.getFieldNumber()) {
+ case (int) GeneratedPreviewsProto.Preview.VIEWS:
+ if (skipViews) {
+ // ProtoInputStream will skip over the nested message when nextField() is
+ // called.
+ continue;
+ }
+ final long token = input.start(GeneratedPreviewsProto.Preview.VIEWS);
+ try {
+ views = RemoteViews.createPreviewFromProto(mContext, input);
+ } catch (Exception e) {
+ Slog.e(TAG, "Unable to deserialize RemoteViews", e);
+ }
+ input.end(token);
+ break;
+ case (int) GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES:
+ widgetCategories = input.readInt(
+ GeneratedPreviewsProto.Preview.WIDGET_CATEGORIES);
+ break;
+ default:
+ Slog.w(TAG, "Unknown field while reading GeneratedPreviewsProto! "
+ + ProtoUtils.currentFieldToString(input));
+ }
+ }
+ return Pair.create(widgetCategories, views);
+ }
+
+ /**
+ * Returns the file in which all generated previews for this provider are stored. This will be
+ * a path of the form:
+ * {@literal /data/system_ce/<userId>/appwidget/previews/<package>-<class>-<uid>.binpb}
+ *
+ * This function will not create the file if it does not already exist.
+ */
+ @NonNull
+ private static AtomicFile getWidgetPreviewsFile(@NonNull Provider provider) throws IOException {
+ int userId = provider.getUserId();
+ File previewsDirectory = getWidgetPreviewsDirectory(userId);
+ File providerPreviews = Environment.buildPath(previewsDirectory,
+ TextUtils.formatSimple("%s-%s-%d.binpb", provider.id.componentName.getPackageName(),
+ provider.id.componentName.getClassName(), provider.id.uid));
+ return new AtomicFile(providerPreviews);
+ }
+
+ /**
+ * Returns the widget previews directory for the given user, creating it if it does not exist.
+ * This will be a path of the form:
+ * {@literal /data/system_ce/<userId>/appwidget/previews}
+ */
+ @NonNull
+ private static File getWidgetPreviewsDirectory(int userId) throws IOException {
+ File dataSystemCeDirectory = Environment.getDataSystemCeDirectory(userId);
+ File previewsDirectory = Environment.buildPath(dataSystemCeDirectory,
+ APPWIDGET_CE_DATA_DIRNAME, WIDGET_PREVIEWS_DIRNAME);
+ if (!previewsDirectory.exists()) {
+ if (!previewsDirectory.mkdirs()) {
+ throw new IOException("Unable to create widget preview directory "
+ + previewsDirectory.getPath());
+ }
+ }
+ return previewsDirectory;
+ }
+
private static void ensureWidgetCategoryCombinationIsValid(int widgetCategories) {
int validCategories = AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
| AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
@@ -5415,11 +5823,11 @@
AppWidgetManager.META_DATA_APPWIDGET_PROVIDER);
}
if (newInfo != null) {
+ newInfo.generatedPreviewCategories = info.generatedPreviewCategories;
info = newInfo;
if (DEBUG) {
Objects.requireNonNull(info);
}
- updateGeneratedPreviewCategoriesLocked();
}
}
mInfoParsed = true;
@@ -5476,7 +5884,7 @@
generatedPreviews.put(flag, preview);
}
}
- updateGeneratedPreviewCategoriesLocked();
+ updateGeneratedPreviewCategoriesLocked(generatedPreviews);
}
@GuardedBy("this.mLock")
@@ -5488,7 +5896,7 @@
}
}
if (changed) {
- updateGeneratedPreviewCategoriesLocked();
+ updateGeneratedPreviewCategoriesLocked(generatedPreviews);
}
return changed;
}
@@ -5497,17 +5905,19 @@
public boolean clearGeneratedPreviewsLocked() {
if (generatedPreviews.size() > 0) {
generatedPreviews.clear();
- updateGeneratedPreviewCategoriesLocked();
+ updateGeneratedPreviewCategoriesLocked(generatedPreviews);
return true;
}
return false;
}
-
@GuardedBy("this.mLock")
- private void updateGeneratedPreviewCategoriesLocked() {
+ private void updateGeneratedPreviewCategoriesLocked(
+ @Nullable SparseArray<RemoteViews> previews) {
info.generatedPreviewCategories = 0;
- for (int i = 0; i < generatedPreviews.size(); i++) {
- info.generatedPreviewCategories |= generatedPreviews.keyAt(i);
+ if (previews != null) {
+ for (int i = 0; i < previews.size(); i++) {
+ info.generatedPreviewCategories |= previews.keyAt(i);
+ }
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 87ce649..2a9cb43 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -134,6 +134,7 @@
import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
import static android.view.Display.INVALID_DISPLAY;
+import static com.android.internal.util.FrameworkStatsLog.INTENT_CREATOR_TOKEN_ADDED;
import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NEW_MUTABLE_IMPLICIT_PENDING_INTENT_RETRIEVED;
import static com.android.sdksandbox.flags.Flags.sdkSandboxInstrumentationInfo;
import static com.android.server.am.ActiveServices.FGS_SAW_RESTRICTIONS;
@@ -2794,9 +2795,6 @@
addServiceToMap(mAppBindArgs, Context.POWER_SERVICE);
addServiceToMap(mAppBindArgs, "mount");
addServiceToMap(mAppBindArgs, Context.PLATFORM_COMPAT_SERVICE);
- addServiceToMap(mAppBindArgs, "permissionmgr");
- addServiceToMap(mAppBindArgs, Context.APP_OPS_SERVICE);
- addServiceToMap(mAppBindArgs, Context.USER_SERVICE);
}
// See b/79378449
// Getting the window service and package service binder from servicemanager
@@ -2804,6 +2802,9 @@
// TODO: remove exception
addServiceToMap(mAppBindArgs, "package");
addServiceToMap(mAppBindArgs, Context.WINDOW_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.USER_SERVICE);
+ addServiceToMap(mAppBindArgs, "permissionmgr");
+ addServiceToMap(mAppBindArgs, Context.APP_OPS_SERVICE);
}
return mAppBindArgs;
}
@@ -19291,12 +19292,14 @@
+ "} does not correspond to an intent in the extra bundle.");
continue;
}
- Slog.wtf(TAG,
- "A creator token is added to an intent. creatorPackage: " + creatorPackage
- + "; intent: " + intent);
- IBinder creatorToken = createIntentCreatorToken(extraIntent, creatorPackage);
+ IntentCreatorToken creatorToken = createIntentCreatorToken(extraIntent,
+ creatorPackage);
if (creatorToken != null) {
extraIntent.setCreatorToken(creatorToken);
+ Slog.wtf(TAG, "A creator token is added to an intent. creatorPackage: "
+ + creatorPackage + "; intent: " + intent);
+ FrameworkStatsLog.write(INTENT_CREATOR_TOKEN_ADDED,
+ creatorToken.getCreatorUid());
}
} catch (Exception e) {
Slog.wtf(TAG,
@@ -19307,7 +19310,7 @@
}
}
- private IBinder createIntentCreatorToken(Intent intent, String creatorPackage) {
+ private IntentCreatorToken createIntentCreatorToken(Intent intent, String creatorPackage) {
if (IntentCreatorToken.isValid(intent)) return null;
int creatorUid = getCallingUid();
IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, creatorPackage, intent);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 7afcb13..69102f1 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -192,6 +192,7 @@
"make_pixel_haptics",
"media_audio",
"media_drm",
+ "media_projection",
"media_reliability",
"media_solutions",
"media_tv",
@@ -204,6 +205,7 @@
"pixel_bluetooth",
"pixel_connectivity_gps",
"pixel_continuity",
+ "pixel_perf",
"pixel_sensors",
"pixel_system_sw_video",
"pixel_watch",
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index dbdc614..906e584 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -2641,8 +2641,8 @@
Log.w(TAG, "failed to broadcast ACTION_SPEAKERPHONE_STATE_CHANGED: " + e);
}
}
- mAudioService.postUpdateRingerModeServiceInt();
dispatchCommunicationDevice();
+ mAudioService.postUpdateRingerModeServiceInt();
}
@GuardedBy("mDeviceStateLock")
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index b2c616a..96c178a 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -112,7 +112,7 @@
throws RemoteException {
final boolean isOnlyMandatoryBiometricsRequested = promptInfo.getAuthenticators()
- == BiometricManager.Authenticators.MANDATORY_BIOMETRICS;
+ == BiometricManager.Authenticators.IDENTITY_CHECK;
boolean isMandatoryBiometricsAuthentication = false;
if (dropCredentialFallback(promptInfo.getAuthenticators(),
@@ -180,8 +180,8 @@
private static boolean dropCredentialFallback(int authenticators,
boolean isMandatoryBiometricsEnabled, ITrustManager trustManager) {
final boolean isMandatoryBiometricsRequested =
- (authenticators & BiometricManager.Authenticators.MANDATORY_BIOMETRICS)
- == BiometricManager.Authenticators.MANDATORY_BIOMETRICS;
+ (authenticators & BiometricManager.Authenticators.IDENTITY_CHECK)
+ == BiometricManager.Authenticators.IDENTITY_CHECK;
if (Flags.mandatoryBiometrics() && isMandatoryBiometricsEnabled
&& isMandatoryBiometricsRequested) {
try {
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 8734136..c1f8e2e 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -147,7 +147,7 @@
* @return true if mandatory biometrics is requested
*/
static boolean isMandatoryBiometricsRequested(@Authenticators.Types int authenticators) {
- return (authenticators & Authenticators.MANDATORY_BIOMETRICS) != 0;
+ return (authenticators & Authenticators.IDENTITY_CHECK) != 0;
}
/**
@@ -257,7 +257,7 @@
if (Flags.mandatoryBiometrics()) {
testBits = ~(Authenticators.DEVICE_CREDENTIAL
| Authenticators.BIOMETRIC_MIN_STRENGTH
- | Authenticators.MANDATORY_BIOMETRICS);
+ | Authenticators.IDENTITY_CHECK);
} else {
testBits = ~(Authenticators.DEVICE_CREDENTIAL
| Authenticators.BIOMETRIC_MIN_STRENGTH);
@@ -329,8 +329,8 @@
case BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED:
biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
break;
- case BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE:
- biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
+ case BiometricConstants.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE:
+ biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE;
break;
case BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS:
biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
@@ -397,7 +397,7 @@
case BIOMETRIC_SENSOR_PRIVACY_ENABLED:
return BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED;
case MANDATORY_BIOMETRIC_UNAVAILABLE_ERROR:
- return BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE;
+ return BiometricConstants.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE;
case BIOMETRIC_NOT_ENABLED_FOR_APPS:
if (Flags.mandatoryBiometrics()) {
return BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 5682c33..bf415a3 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -797,6 +797,10 @@
@Override
public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
for (HdmiDeviceInfo info : deviceInfos) {
+ if (!isInputReady(info.getDeviceId())) {
+ mService.getHdmiCecNetwork().removeCecDevice(
+ HdmiCecLocalDeviceTv.this, info.getLogicalAddress());
+ }
mService.getHdmiCecNetwork().addCecDevice(info);
}
diff --git a/services/core/java/com/android/server/inputmethod/ImeBindingState.java b/services/core/java/com/android/server/inputmethod/ImeBindingState.java
index f78ea84..5deed39 100644
--- a/services/core/java/com/android/server/inputmethod/ImeBindingState.java
+++ b/services/core/java/com/android/server/inputmethod/ImeBindingState.java
@@ -20,6 +20,7 @@
import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.os.IBinder;
@@ -86,10 +87,10 @@
InputMethodDebug.softInputModeToString(mFocusedWindowSoftInputMode));
}
- void dump(String prefix, Printer p) {
- p.println(prefix + "mFocusedWindow()=" + mFocusedWindow);
- p.println(prefix + "softInputMode=" + InputMethodDebug.softInputModeToString(
- mFocusedWindowSoftInputMode));
+ void dump(@NonNull Printer p, @NonNull String prefix) {
+ p.println(prefix + "mFocusedWindow=" + mFocusedWindow);
+ p.println(prefix + "mFocusedWindowSoftInputMode="
+ + InputMethodDebug.softInputModeToString(mFocusedWindowSoftInputMode));
p.println(prefix + "mFocusedWindowClient=" + mFocusedWindowClient);
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index ec1993a..477660d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -61,6 +61,7 @@
import com.android.server.EventLogTags;
import com.android.server.wm.WindowManagerInternal;
+import java.io.PrintWriter;
import java.util.concurrent.CountDownLatch;
/**
@@ -733,4 +734,25 @@
void setBackDisposition(@BackDispositionMode int backDisposition) {
mBackDisposition = backDisposition;
}
+
+ @GuardedBy("ImfLock.class")
+ void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+ pw.println(prefix + "mSelectedMethodId=" + mSelectedMethodId);
+ pw.println(prefix + "mCurrentSubtype=" + mCurrentSubtype);
+ pw.println(prefix + "mCurSeq=" + mCurSeq);
+ pw.println(prefix + "mCurId=" + mCurId);
+ pw.println(prefix + "mHasMainConnection=" + mHasMainConnection);
+ pw.println(prefix + "mVisibleBound=" + mVisibleBound);
+ pw.println(prefix + "mCurToken=" + mCurToken);
+ pw.println(prefix + "mCurTokenDisplayId=" + mCurTokenDisplayId);
+ pw.println(prefix + "mCurHostInputToken=" + getCurHostInputToken());
+ pw.println(prefix + "mCurIntent=" + mCurIntent);
+ pw.println(prefix + "mCurMethod=" + mCurMethod);
+ pw.println(prefix + "mImeWindowVis=" + mImeWindowVis);
+ pw.println(prefix + "mBackDisposition=" + mBackDisposition);
+ pw.println(prefix + "mDisplayIdToShowIme=" + mDisplayIdToShowIme);
+ pw.println(prefix + "mDeviceIdToShowIme=" + mDeviceIdToShowIme);
+ pw.println(prefix + "mSupportsStylusHw=" + mSupportsStylusHw);
+ pw.println(prefix + "mSupportsConnectionlessStylusHw=" + mSupportsConnectionlessStylusHw);
+ }
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 83044c2..131b9ba 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -6063,42 +6063,40 @@
@BinderThread
@Override
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
PriorityDump.dump(mPriorityDumper, fd, pw, args);
}
@BinderThread
- private void dumpAsStringNoCheck(FileDescriptor fd, PrintWriter pw, String[] args,
- boolean isCritical) {
+ private void dumpAsStringNoCheck(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
+ @NonNull String[] args, boolean isCritical) {
final int argUserId = parseUserIdFromDumpArgs(args);
final Printer p = new PrintWriterPrinter(pw);
- p.println("Current Input Method Manager state:");
+ p.println("Input Method Manager Service state:");
p.println(" mSystemReady=" + mSystemReady);
p.println(" mInteractive=" + mIsInteractive);
p.println(" mConcurrentMultiUserModeEnabled=" + mConcurrentMultiUserModeEnabled);
p.println(" ENABLE_HIDE_IME_CAPTION_BAR="
+ InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR);
+ final int currentImeUserId;
synchronized (ImfLock.class) {
+ currentImeUserId = mCurrentImeUserId;
+ p.println(" mCurrentImeUserId=" + currentImeUserId);
p.println(" mStylusIds=" + (mStylusIds != null
? Arrays.toString(mStylusIds.toArray()) : ""));
}
+ // TODO(b/305849394): Make mMenuController multi-user aware.
if (Flags.imeSwitcherRevamp()) {
p.println(" mMenuControllerNew:");
- mMenuControllerNew.dump(p, " ");
+ mMenuControllerNew.dump(p, " ");
} else {
p.println(" mMenuController:");
- mMenuController.dump(p, " ");
+ mMenuController.dump(p, " ");
}
- if (mConcurrentMultiUserModeEnabled && argUserId == UserHandle.USER_NULL) {
- mUserDataRepository.forAllUserData(
- u -> dumpAsStringNoCheckForUser(u, fd, pw, args, isCritical));
- } else {
- final int userId = argUserId != UserHandle.USER_NULL ? argUserId : mCurrentImeUserId;
- final var userData = getUserData(userId);
- dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
- }
+ dumpClientController(p);
+ dumpUserRepository(p);
// TODO(b/365868861): Make StartInputHistory and ImeTracker multi-user aware.
synchronized (ImfLock.class) {
@@ -6112,12 +6110,18 @@
p.println(" mImeTrackerService#History:");
mImeTrackerService.dump(pw, " ");
- dumpUserRepository(p);
- dumpClientStates(p);
+ if (mConcurrentMultiUserModeEnabled && argUserId == UserHandle.USER_NULL) {
+ mUserDataRepository.forAllUserData(
+ u -> dumpAsStringNoCheckForUser(u, fd, pw, args, isCritical));
+ } else {
+ final int userId = argUserId != UserHandle.USER_NULL ? argUserId : currentImeUserId;
+ final var userData = getUserData(userId);
+ dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
+ }
}
@UserIdInt
- private static int parseUserIdFromDumpArgs(String[] args) {
+ private static int parseUserIdFromDumpArgs(@NonNull String[] args) {
final int userIdx = Arrays.binarySearch(args, "--user");
if (userIdx == -1 || userIdx == args.length - 1) {
return UserHandle.USER_NULL;
@@ -6127,44 +6131,37 @@
// TODO(b/356239178): Update dump format output to better group per-user info.
@BinderThread
- private void dumpAsStringNoCheckForUser(UserData userData, FileDescriptor fd, PrintWriter pw,
- String[] args, boolean isCritical) {
+ private void dumpAsStringNoCheckForUser(@NonNull UserData userData, @NonNull FileDescriptor fd,
+ @NonNull PrintWriter pw, @NonNull String[] args, boolean isCritical) {
final Printer p = new PrintWriterPrinter(pw);
- IInputMethodInvoker method;
ClientState client;
+ IInputMethodInvoker method;
p.println(" UserId=" + userData.mUserId);
synchronized (ImfLock.class) {
- final InputMethodSettings settings = InputMethodSettingsRepository.get(
- userData.mUserId);
+ final var bindingController = userData.mBindingController;
+ client = userData.mCurClient;
+ method = bindingController.getCurMethod();
+ p.println(" mBindingController:");
+ bindingController.dump(pw, " ");
+ p.println(" mCurClient=" + client);
+ p.println(" mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
+ p.println(" mImeBindingState:");
+ userData.mImeBindingState.dump(p, " ");
+ p.println(" mBoundToMethod=" + userData.mBoundToMethod);
+ p.println(" mEnabledSession=" + userData.mEnabledSession);
+ p.println(" mVisibilityStateComputer:");
+ userData.mVisibilityStateComputer.dump(pw, " ");
+ p.println(" mInFullscreenMode=" + userData.mInFullscreenMode);
+
+ final var settings = InputMethodSettingsRepository.get(userData.mUserId);
final List<InputMethodInfo> methodList = settings.getMethodList();
- int numImes = methodList.size();
+ final int numImes = methodList.size();
p.println(" Input Methods:");
for (int i = 0; i < numImes; i++) {
- InputMethodInfo info = methodList.get(i);
+ final InputMethodInfo info = methodList.get(i);
p.println(" InputMethod #" + i + ":");
info.dump(p, " ");
}
- final var bindingController = userData.mBindingController;
- p.println(" mCurMethodId=" + bindingController.getSelectedMethodId());
- client = userData.mCurClient;
- p.println(" mCurClient=" + client + " mCurSeq="
- + bindingController.getSequenceNumber());
- p.println(" mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
- userData.mImeBindingState.dump(/* prefix= */ " ", p);
- p.println(" mCurId=" + bindingController.getCurId());
- p.println(" mHaveConnection=" + bindingController.hasMainConnection());
- p.println(" mBoundToMethod=" + userData.mBoundToMethod);
- p.println(" mVisibleBound=" + bindingController.isVisibleBound());
- p.println(" mCurToken=" + bindingController.getCurToken());
- p.println(" mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId());
- p.println(" mCurHostInputToken=" + bindingController.getCurHostInputToken());
- p.println(" mCurIntent=" + bindingController.getCurIntent());
- method = bindingController.getCurMethod();
- p.println(" mCurMethod=" + method);
- p.println(" mEnabledSession=" + userData.mEnabledSession);
- final var visibilityStateComputer = userData.mVisibilityStateComputer;
- visibilityStateComputer.dump(pw, " ");
- p.println(" mInFullscreenMode=" + userData.mInFullscreenMode);
}
// Exit here for critical dump, as remaining sections require IPCs to other processes.
@@ -6172,7 +6169,7 @@
return;
}
- p.println(" ");
+ p.println("");
if (client != null) {
pw.flush();
try {
@@ -6184,25 +6181,23 @@
p.println("No input method client.");
}
synchronized (ImfLock.class) {
- if (userData.mImeBindingState.mFocusedWindowClient != null
- && client != userData.mImeBindingState.mFocusedWindowClient) {
- p.println(" ");
- p.println("Warning: Current input method client doesn't match the last focused. "
- + "window.");
+ final var focusedWindowClient = userData.mImeBindingState.mFocusedWindowClient;
+ if (focusedWindowClient != null && client != focusedWindowClient) {
+ p.println("");
+ p.println("Warning: Current input method client doesn't match the last focused"
+ + " window.");
p.println("Dumping input method client in the last focused window just in case.");
- p.println(" ");
+ p.println("");
pw.flush();
try {
- TransferPipe.dumpAsync(
- userData.mImeBindingState.mFocusedWindowClient.mClient.asBinder(), fd,
- args);
+ TransferPipe.dumpAsync(focusedWindowClient.mClient.asBinder(), fd, args);
} catch (IOException | RemoteException e) {
p.println("Failed to dump input method client in focused window: " + e);
}
}
}
- p.println(" ");
+ p.println("");
if (method != null) {
pw.flush();
try {
@@ -6215,56 +6210,51 @@
}
}
- private void dumpClientStates(Printer p) {
- p.println(" ClientStates:");
+ private void dumpClientController(@NonNull Printer p) {
+ p.println(" mClientController:");
// TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
@SuppressWarnings("GuardedBy") Consumer<ClientState> clientControllerDump = c -> {
- p.println(" " + c + ":");
- p.println(" client=" + c.mClient);
- p.println(" fallbackInputConnection="
- + c.mFallbackInputConnection);
- p.println(" sessionRequested="
- + c.mSessionRequested);
- p.println(" sessionRequestedForAccessibility="
+ p.println(" " + c + ":");
+ p.println(" client=" + c.mClient);
+ p.println(" fallbackInputConnection=" + c.mFallbackInputConnection);
+ p.println(" sessionRequested=" + c.mSessionRequested);
+ p.println(" sessionRequestedForAccessibility="
+ c.mSessionRequestedForAccessibility);
- p.println(" curSession=" + c.mCurSession);
- p.println(" selfReportedDisplayId=" + c.mSelfReportedDisplayId);
- p.println(" uid=" + c.mUid);
- p.println(" pid=" + c.mPid);
+ p.println(" curSession=" + c.mCurSession);
+ p.println(" selfReportedDisplayId=" + c.mSelfReportedDisplayId);
+ p.println(" uid=" + c.mUid);
+ p.println(" pid=" + c.mPid);
};
synchronized (ImfLock.class) {
mClientController.forAllClients(clientControllerDump);
}
}
- private void dumpUserRepository(Printer p) {
- p.println(" mUserDataRepository=");
+ private void dumpUserRepository(@NonNull Printer p) {
+ p.println(" mUserDataRepository:");
// TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
- @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump =
- u -> {
- p.println(" mUserId=" + u.mUserId);
- p.println(" unlocked=" + u.mIsUnlockingOrUnlocked.get());
- p.println(" hasMainConnection="
- + u.mBindingController.hasMainConnection());
- p.println(" isVisibleBound=" + u.mBindingController.isVisibleBound());
- p.println(" boundToMethod=" + u.mBoundToMethod);
- p.println(" curClient=" + u.mCurClient);
- if (u.mCurEditorInfo != null) {
- p.println(" curEditorInfo:");
- u.mCurEditorInfo.dump(p, " ", false /* dumpExtras */);
- } else {
- p.println(" curEditorInfo: null");
- }
- p.println(" imeBindingState:");
- u.mImeBindingState.dump(" ", p);
- p.println(" enabledSession=" + u.mEnabledSession);
- p.println(" inFullscreenMode=" + u.mInFullscreenMode);
- p.println(" imeDrawsNavBar=" + u.mImeDrawsNavBar.get());
- p.println(" switchingController:");
- u.mSwitchingController.dump(p, " ");
- p.println(" mLastEnabledInputMethodsStr="
- + u.mLastEnabledInputMethodsStr);
- };
+ @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump = u -> {
+ p.println(" userId=" + u.mUserId);
+ p.println(" unlocked=" + u.mIsUnlockingOrUnlocked.get());
+ p.println(" hasMainConnection=" + u.mBindingController.hasMainConnection());
+ p.println(" isVisibleBound=" + u.mBindingController.isVisibleBound());
+ p.println(" boundToMethod=" + u.mBoundToMethod);
+ p.println(" curClient=" + u.mCurClient);
+ if (u.mCurEditorInfo != null) {
+ p.println(" curEditorInfo:");
+ u.mCurEditorInfo.dump(p, " ", false /* dumpExtras */);
+ } else {
+ p.println(" curEditorInfo: null");
+ }
+ p.println(" imeBindingState:");
+ u.mImeBindingState.dump(p, " ");
+ p.println(" enabledSession=" + u.mEnabledSession);
+ p.println(" inFullscreenMode=" + u.mInFullscreenMode);
+ p.println(" imeDrawsNavBar=" + u.mImeDrawsNavBar.get());
+ p.println(" switchingController:");
+ u.mSwitchingController.dump(p, " ");
+ p.println(" mLastEnabledInputMethodsStr=" + u.mLastEnabledInputMethodsStr);
+ };
synchronized (ImfLock.class) {
mUserDataRepository.forAllUserData(userDataDump);
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index b5ee068..248fa60 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -287,10 +287,10 @@
void dump(@NonNull Printer pw, @NonNull String prefix) {
final boolean showing = isisInputMethodPickerShownForTestLocked();
- pw.println(prefix + " isShowing: " + showing);
+ pw.println(prefix + "isShowing: " + showing);
if (showing) {
- pw.println(prefix + " imList: " + mImList);
+ pw.println(prefix + "imList: " + mImList);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
index 1d0e3c6..6abd5aa 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -187,10 +187,10 @@
void dump(@NonNull Printer pw, @NonNull String prefix) {
final boolean showing = isShowing();
- pw.println(prefix + " isShowing: " + showing);
+ pw.println(prefix + "isShowing: " + showing);
if (showing) {
- pw.println(prefix + " menuItems: " + mMenuItems);
+ pw.println(prefix + "menuItems: " + mMenuItems);
}
}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 19ac1ec..58b1e49 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -71,6 +71,7 @@
import android.app.admin.DevicePolicyManagerInternal;
import android.companion.virtual.VirtualDeviceManager;
import android.content.ComponentName;
+import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -649,11 +650,11 @@
int userId, int callingUid, int callingPid,
boolean includeInstantApps, boolean resolveForStart) {
if (!mUserManager.exists(userId)) return Collections.emptyList();
- enforceCrossUserOrProfilePermission(callingUid,
+ enforceCrossUserOrProfilePermission(Binder.getCallingUid(),
userId,
false /*requireFullPermission*/,
false /*checkShell*/,
- "query intent receivers");
+ "query intent services");
final String instantAppPkgName = getInstantAppPackageName(callingUid);
flags = updateFlagsForResolve(flags, userId, callingUid, includeInstantApps,
false /* isImplicitImageCaptureIntentAndNotSetByDpc */);
@@ -2208,10 +2209,10 @@
return true;
}
boolean permissionGranted = requireFullPermission ? hasPermission(
- Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
: (hasPermission(
- android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
- || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS));
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+ || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS, callingUid));
if (!permissionGranted) {
if (Process.isIsolatedUid(callingUid) && isKnownIsolatedComputeApp(callingUid)) {
return checkIsolatedOwnerHasPermission(callingUid, requireFullPermission);
@@ -4669,7 +4670,7 @@
if (!forceAllowCrossUser) {
enforceCrossUserPermission(
- callingUid,
+ Binder.getCallingUid(),
userId,
false /* requireFullPermission */,
false /* checkShell */,
@@ -4752,8 +4753,14 @@
int callingUid) {
if (!mUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId);
- final ProviderInfo providerInfo = mComponentResolver.queryProvider(this, name, flags,
- userId);
+
+ // Callers of this API may not always separate the userID and authority. Let's parse it
+ // before resolving
+ String authorityWithoutUserId = ContentProvider.getAuthorityWithoutUserId(name);
+ userId = ContentProvider.getUserIdFromAuthority(name, userId);
+
+ final ProviderInfo providerInfo = mComponentResolver.queryProvider(this,
+ authorityWithoutUserId, flags, userId);
boolean checkedGrants = false;
if (providerInfo != null) {
// Looking for cross-user grants before enforcing the typical cross-users permissions
@@ -4767,7 +4774,7 @@
if (!checkedGrants) {
boolean enforceCrossUser = true;
- if (isAuthorityRedirectedForCloneProfile(name)) {
+ if (isAuthorityRedirectedForCloneProfile(authorityWithoutUserId)) {
final UserManagerInternal umInternal = mInjector.getUserManagerInternal();
UserInfo userInfo = umInternal.getUserInfo(UserHandle.getUserId(callingUid));
@@ -5242,7 +5249,7 @@
@Override
public int getComponentEnabledSetting(@NonNull ComponentName component, int callingUid,
@UserIdInt int userId) {
- enforceCrossUserPermission(callingUid, userId, false /*requireFullPermission*/,
+ enforceCrossUserPermission(Binder.getCallingUid(), userId, false /*requireFullPermission*/,
false /*checkShell*/, "getComponentEnabled");
return getComponentEnabledSettingInternal(component, callingUid, userId);
}
diff --git a/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java
new file mode 100644
index 0000000..d0d6071
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.annotation.NonNull;
+import android.os.Trace;
+import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a step to turn the vibrator on using a composition of PWLE segments.
+ *
+ * <p>This step will use the maximum supported number of consecutive segments of type
+ * {@link PwleSegment}, starting at the current index.
+ */
+final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep {
+
+ ComposePwleV2VibratorStep(VibrationStepConductor conductor, long startTime,
+ VibratorController controller, VibrationEffect.Composed effect, int index,
+ long pendingVibratorOffDeadline) {
+ // This step should wait for the last vibration to finish (with the timeout) and for the
+ // intended step start time (to respect the effect delays).
+ super(conductor, Math.max(startTime, pendingVibratorOffDeadline), controller, effect,
+ index, pendingVibratorOffDeadline);
+ }
+
+ @NonNull
+ @Override
+ public List<Step> play() {
+ if (!Flags.normalizedPwleEffects()) {
+ // Skip this step and play the next one right away.
+ return nextSteps(/* segmentsPlayed= */ 1);
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "ComposePwleV2Step");
+ try {
+ // Load the next PwleSegments to create a single composePwleV2 call to the vibrator,
+ // limited to the vibrator's maximum envelope effect size.
+ int limit = controller.getVibratorInfo().getMaxEnvelopeEffectSize();
+ List<PwleSegment> pwles = unrollPwleSegments(effect, segmentIndex, limit);
+
+ if (pwles.isEmpty()) {
+ Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposeEnvelopeStep: "
+ + effect.getSegments().get(segmentIndex));
+ // Skip this step and play the next one right away.
+ return nextSteps(/* segmentsPlayed= */ 1);
+ }
+
+ if (VibrationThread.DEBUG) {
+ Slog.d(VibrationThread.TAG, "Compose " + pwles + " PWLEs on vibrator "
+ + controller.getVibratorInfo().getId());
+ }
+ PwleSegment[] pwlesArray = pwles.toArray(new PwleSegment[pwles.size()]);
+ long vibratorOnResult = controller.on(pwlesArray, getVibration().id);
+ handleVibratorOnResult(vibratorOnResult);
+ getVibration().stats.reportComposePwle(vibratorOnResult, pwlesArray);
+
+ // The next start and off times will be calculated from mVibratorOnResult.
+ return nextSteps(/* segmentsPlayed= */ pwles.size());
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ }
+ }
+
+ private List<PwleSegment> unrollPwleSegments(VibrationEffect.Composed effect, int startIndex,
+ int limit) {
+ List<PwleSegment> segments = new ArrayList<>(limit);
+ float bestBreakAmplitude = 1;
+ int bestBreakPosition = limit; // Exclusive index.
+
+ int segmentCount = effect.getSegments().size();
+ int repeatIndex = effect.getRepeatIndex();
+
+ // Loop once after reaching the limit to see if breaking it will really be necessary, then
+ // apply the best break position found, otherwise return the full list as it fits the limit.
+ for (int i = startIndex; segments.size() <= limit; i++) {
+ if (i == segmentCount) {
+ if (repeatIndex >= 0) {
+ i = repeatIndex;
+ } else {
+ // Non-repeating effect, stop collecting pwles.
+ break;
+ }
+ }
+ VibrationEffectSegment segment = effect.getSegments().get(i);
+ if (segment instanceof PwleSegment pwleSegment) {
+ segments.add(pwleSegment);
+
+ if (isBetterBreakPosition(segments, bestBreakAmplitude, limit)) {
+ // Mark this position as the best one so far to break a long waveform.
+ bestBreakAmplitude = pwleSegment.getEndAmplitude();
+ bestBreakPosition = segments.size(); // Break after this pwle ends.
+ }
+ } else {
+ // First non-pwle segment, stop collecting pwles.
+ break;
+ }
+ }
+
+ return segments.size() > limit
+ // Remove excessive segments, using the best breaking position recorded.
+ ? segments.subList(0, bestBreakPosition)
+ // Return all collected pwle segments.
+ : segments;
+ }
+
+ /**
+ * Returns true if the current segment list represents a better break position for a PWLE,
+ * given the current amplitude being used for breaking it at a smaller size and the size limit.
+ */
+ private boolean isBetterBreakPosition(List<PwleSegment> segments,
+ float currentBestBreakAmplitude, int limit) {
+ PwleSegment lastSegment = segments.get(segments.size() - 1);
+ float breakAmplitudeCandidate = lastSegment.getEndAmplitude();
+ int breakPositionCandidate = segments.size();
+
+ if (breakPositionCandidate > limit) {
+ // We're beyond limit, last break position found should be used.
+ return false;
+ }
+ if (breakAmplitudeCandidate == 0) {
+ // Breaking at amplitude zero at any position is always preferable.
+ return true;
+ }
+ if (breakPositionCandidate < limit / 2) {
+ // Avoid breaking at the first half of the allowed maximum size, even if amplitudes are
+ // lower, to avoid creating PWLEs that are too small unless it's to break at zero.
+ return false;
+ }
+ // Prefer lower amplitudes at a later position for breaking the PWLE in a more subtle way.
+ return breakAmplitudeCandidate <= currentBestBreakAmplitude;
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/DeviceAdapter.java b/services/core/java/com/android/server/vibrator/DeviceAdapter.java
index bd4fc07..751e83c 100644
--- a/services/core/java/com/android/server/vibrator/DeviceAdapter.java
+++ b/services/core/java/com/android/server/vibrator/DeviceAdapter.java
@@ -47,6 +47,11 @@
* instance is created with the final segment list.
*/
private final List<VibrationSegmentsAdapter> mSegmentAdapters;
+ /**
+ * The vibration segment validators that can validate VibrationEffectSegments entries based on
+ * the VibratorInfo.
+ */
+ private final List<VibrationSegmentsValidator> mSegmentsValidators;
DeviceAdapter(VibrationSettings settings, SparseArray<VibratorController> vibrators) {
mSegmentAdapters = Arrays.asList(
@@ -60,7 +65,13 @@
// Split segments based on their duration and device supported limits
new SplitSegmentsAdapter(),
// Clip amplitudes and frequencies of final segments based on device bandwidth curve
- new ClippingAmplitudeAndFrequencyAdapter()
+ new ClippingAmplitudeAndFrequencyAdapter(),
+ // Split Pwle segments based on their duration and device supported limits
+ new SplitPwleSegmentsAdapter()
+ );
+ mSegmentsValidators = List.of(
+ // Validate Pwle segments base on the vibrators frequency range
+ new PwleSegmentsValidator()
);
mAvailableVibrators = vibrators;
mAvailableVibratorIds = new int[vibrators.size()];
@@ -78,7 +89,6 @@
return mAvailableVibratorIds;
}
- @NonNull
@Override
public VibrationEffect adaptToVibrator(int vibratorId, @NonNull VibrationEffect effect) {
if (!(effect instanceof VibrationEffect.Composed composed)) {
@@ -102,6 +112,14 @@
mSegmentAdapters.get(i).adaptToVibrator(info, newSegments, newRepeatIndex);
}
+ // Validate the vibration segments. If a segment is not supported, ignore the entire
+ // vibration effect.
+ for (int i = 0; i < mSegmentsValidators.size(); i++) {
+ if (!mSegmentsValidators.get(i).hasValidSegments(info, newSegments)) {
+ return null;
+ }
+ }
+
return new VibrationEffect.Composed(newSegments, newRepeatIndex);
}
}
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index d192e64..c9f1e4b 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -124,12 +124,18 @@
* @param deviceAdapter A {@link CombinedVibration.VibratorAdapter} that transforms vibration
* effects to device vibrators based on its capabilities.
*/
- public void adaptToDevice(CombinedVibration.VibratorAdapter deviceAdapter) {
- CombinedVibration newEffect = mEffectToPlay.adapt(deviceAdapter);
- if (!Objects.equals(mEffectToPlay, newEffect)) {
- mEffectToPlay = newEffect;
+ public boolean adaptToDevice(CombinedVibration.VibratorAdapter deviceAdapter) {
+ CombinedVibration adaptedEffect = mEffectToPlay.adapt(deviceAdapter);
+ if (adaptedEffect == null) {
+ return false;
+ }
+
+ if (!mEffectToPlay.equals(adaptedEffect)) {
+ mEffectToPlay = adaptedEffect;
}
// No need to update fallback effects, they are already configured per device.
+
+ return true;
}
/** Return the effect that should be played by this vibration. */
diff --git a/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java b/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java
new file mode 100644
index 0000000..87369aa
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/PwleSegmentsValidator.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibratorInfo;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.VibrationEffectSegment;
+
+import java.util.List;
+
+/**
+ * Validates Pwle segments to ensure they are compatible with the device's capabilities
+ * and adhere to frequency constraints.
+ *
+ * <p>The validator verifies that each segment's start and end frequencies fall within
+ * the supported range.
+ *
+ * <p>The segments will be considered invalid of the device does not have
+ * {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
+ */
+final class PwleSegmentsValidator implements VibrationSegmentsValidator {
+
+ @Override
+ public boolean hasValidSegments(VibratorInfo info, List<VibrationEffectSegment> segments) {
+
+ boolean hasPwleCapability = info.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+ float minFrequency = info.getFrequencyProfile().getMinFrequencyHz();
+ float maxFrequency = info.getFrequencyProfile().getMaxFrequencyHz();
+
+ for (VibrationEffectSegment segment : segments) {
+ if (!(segment instanceof PwleSegment pwleSegment)) {
+ continue;
+ }
+
+ if (!hasPwleCapability || pwleSegment.getStartFrequencyHz() < minFrequency
+ || pwleSegment.getStartFrequencyHz() > maxFrequency
+ || pwleSegment.getEndFrequencyHz() < minFrequency
+ || pwleSegment.getEndFrequencyHz() > maxFrequency) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/SplitPwleSegmentsAdapter.java b/services/core/java/com/android/server/vibrator/SplitPwleSegmentsAdapter.java
new file mode 100644
index 0000000..ad44227
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/SplitPwleSegmentsAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.hardware.vibrator.IVibrator;
+import android.os.VibratorInfo;
+import android.os.vibrator.PwleSegment;
+import android.os.vibrator.VibrationEffectSegment;
+import android.util.MathUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter that splits Pwle segments with longer duration than the device capabilities.
+ *
+ * <p>This transformation replaces large {@link android.os.vibrator.PwleSegment} entries by a
+ * sequence of smaller segments that starts and ends at the same amplitudes/frequencies,
+ * interpolating the intermediate values.
+ *
+ * <p>The segments will not be changed if the device doesn't have
+ * {@link IVibrator#CAP_COMPOSE_PWLE_EFFECTS_V2}.
+ */
+final class SplitPwleSegmentsAdapter implements VibrationSegmentsAdapter {
+
+ @Override
+ public int adaptToVibrator(VibratorInfo info, List<VibrationEffectSegment> segments,
+ int repeatIndex) {
+ if (!info.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)) {
+ // The vibrator does not have PWLE v2 capability, so keep the segments unchanged.
+ return repeatIndex;
+ }
+ int maxPwleDuration = info.getMaxEnvelopeEffectDurationMillis();
+ if (maxPwleDuration <= 0) {
+ // No limit set to PWLE primitive duration.
+ return repeatIndex;
+ }
+
+ int segmentCount = segments.size();
+ for (int i = 0; i < segmentCount; i++) {
+ if (!(segments.get(i) instanceof PwleSegment pwleSegment)) {
+ continue;
+ }
+ int splits = ((int) pwleSegment.getDuration() + maxPwleDuration - 1) / maxPwleDuration;
+ if (splits <= 1) {
+ continue;
+ }
+ segments.remove(i);
+ segments.addAll(i, splitPwleSegment(pwleSegment, splits));
+ int addedSegments = splits - 1;
+ if (repeatIndex > i) {
+ repeatIndex += addedSegments;
+ }
+ i += addedSegments;
+ segmentCount += addedSegments;
+ }
+
+ return repeatIndex;
+ }
+
+ private static List<PwleSegment> splitPwleSegment(PwleSegment pwleSegment,
+ int splits) {
+ List<PwleSegment> pwleSegments = new ArrayList<>(splits);
+ float startFrequencyHz = pwleSegment.getStartFrequencyHz();
+ float endFrequencyHz = pwleSegment.getEndFrequencyHz();
+ long splitDuration = pwleSegment.getDuration() / splits;
+ float previousAmplitude = pwleSegment.getStartAmplitude();
+ float previousFrequencyHz = startFrequencyHz;
+ long accumulatedDuration = 0;
+
+ for (int i = 1; i < splits; i++) {
+ accumulatedDuration += splitDuration;
+ float durationRatio = (float) accumulatedDuration / pwleSegment.getDuration();
+ float interpolatedFrequency =
+ MathUtils.lerp(startFrequencyHz, endFrequencyHz, durationRatio);
+ float interpolatedAmplitude = MathUtils.lerp(pwleSegment.getStartAmplitude(),
+ pwleSegment.getEndAmplitude(), durationRatio);
+ PwleSegment pwleSplit = new PwleSegment(
+ previousAmplitude, interpolatedAmplitude,
+ previousFrequencyHz, interpolatedFrequency,
+ (int) splitDuration);
+ pwleSegments.add(pwleSplit);
+ previousAmplitude = pwleSplit.getEndAmplitude();
+ previousFrequencyHz = pwleSplit.getEndFrequencyHz();
+ }
+
+ pwleSegments.add(
+ new PwleSegment(previousAmplitude, pwleSegment.getEndAmplitude(),
+ previousFrequencyHz, endFrequencyHz,
+ (int) (pwleSegment.getDuration() - accumulatedDuration)));
+
+ return pwleSegments;
+ }
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationSegmentsValidator.java b/services/core/java/com/android/server/vibrator/VibrationSegmentsValidator.java
new file mode 100644
index 0000000..75002bf
--- /dev/null
+++ b/services/core/java/com/android/server/vibrator/VibrationSegmentsValidator.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vibrator;
+
+import android.os.VibratorInfo;
+import android.os.vibrator.VibrationEffectSegment;
+
+import java.util.List;
+
+/** Validates a sequence of {@link VibrationEffectSegment}s for a vibrator. */
+public interface VibrationSegmentsValidator {
+ /**
+ * Checks whether the vibrator can play the provided segments based on the given
+ * {@link VibratorInfo}.
+ *
+ * @param info The vibrator info to be applied to the sequence of segments.
+ * @param segments List of {@link VibrationEffectSegment} to be checked.
+ * @return True if vibrator can play the effect, false otherwise.
+ */
+ boolean hasValidSegments(VibratorInfo info, List<VibrationEffectSegment> segments);
+}
diff --git a/services/core/java/com/android/server/vibrator/VibrationStats.java b/services/core/java/com/android/server/vibrator/VibrationStats.java
index 637a5a1..bc4dbe7 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStats.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStats.java
@@ -22,6 +22,7 @@
import android.os.SystemClock;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -292,6 +293,25 @@
}
}
+ /** Report a call to vibrator method to trigger a vibration as a PWLE. */
+ void reportComposePwle(long halResult, PwleSegment[] segments) {
+ mVibratorComposePwleCount++;
+ mVibrationPwleTotalSize += segments.length;
+
+ if (halResult > 0) {
+ // If HAL result is positive then it represents the actual duration of the vibration.
+ // Remove the zero-amplitude segments to update the total time the vibrator was ON.
+ for (PwleSegment ramp : segments) {
+ if ((ramp.getStartAmplitude() == 0) && (ramp.getEndAmplitude() == 0)) {
+ halResult -= ramp.getDuration();
+ }
+ }
+ if (halResult > 0) {
+ mVibratorOnTotalDurationMillis += (int) halResult;
+ }
+ }
+ }
+
/**
* Increment the stats for total number of times the {@code setExternalControl} method was
* triggered in the vibrator HAL.
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 4bb0c16..6a4790d 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -24,6 +24,7 @@
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.IntArray;
@@ -166,12 +167,20 @@
return new ComposePwleVibratorStep(this, startTime, controller, effect, segmentIndex,
pendingVibratorOffDeadline);
}
+ if (segment instanceof PwleSegment) {
+ return new ComposePwleV2VibratorStep(this, startTime, controller, effect,
+ segmentIndex, pendingVibratorOffDeadline);
+ }
return new SetAmplitudeVibratorStep(this, startTime, controller, effect, segmentIndex,
pendingVibratorOffDeadline);
}
- /** Called when this conductor is going to be started running by the VibrationThread. */
- public void prepareToStart() {
+ /**
+ * Called when this conductor is going to be started running by the VibrationThread.
+ *
+ * @return True if the vibration effect can be played, false otherwise.
+ */
+ public boolean prepareToStart() {
if (Build.IS_DEBUGGABLE) {
expectIsVibrationThread(true);
}
@@ -182,7 +191,11 @@
// Scale resolves the default amplitudes from the effect before scaling them.
mVibration.scaleEffects(mVibrationScaler);
- mVibration.adaptToDevice(mDeviceAdapter);
+ if (!mVibration.adaptToDevice(mDeviceAdapter)) {
+ // Unable to adapt vibration effect for playback. This likely indicates the presence
+ // of unsupported segments. The original effect will be ignored.
+ return false;
+ }
CombinedVibration.Sequential sequentialEffect = toSequential(mVibration.getEffectToPlay());
mPendingVibrateSteps++;
// This count is decremented at the completion of the step, so we don't subtract one.
@@ -191,6 +204,8 @@
// Vibration will start playing in the Vibrator, following the effect timings and delays.
// Report current time as the vibration start time, for debugging.
mVibration.stats.reportStarted();
+
+ return true;
}
public HalVibration getVibration() {
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 5b22c10..cb9988f 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -263,7 +263,15 @@
private void playVibration() {
Trace.traceBegin(TRACE_TAG_VIBRATOR, "playVibration");
try {
- mExecutingConductor.prepareToStart();
+ if (!mExecutingConductor.prepareToStart()) {
+ // The effect cannot be played, start clean-up tasks and notify
+ // callback immediately.
+ clientVibrationCompleteIfNotAlready(
+ new Vibration.EndInfo(Status.IGNORED_UNSUPPORTED));
+
+ return;
+ }
+
while (!mExecutingConductor.isFinished()) {
boolean readyToRun = mExecutingConductor.waitUntilNextStepIsDue();
// If we waited, don't run the next step, but instead re-evaluate status.
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 6aed00e..f78bff8 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -30,6 +30,7 @@
import android.os.VibratorInfo;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import android.util.IndentingPrintWriter;
import android.util.Slog;
@@ -414,6 +415,33 @@
}
/**
+ * Plays a composition of pwle v2 primitives, using {@code vibrationId} for completion callback
+ * to {@link OnVibrationCompleteListener}.
+ *
+ * <p>This will affect the state of {@link #isVibrating()}.
+ *
+ * @return The duration of the effect playing, or 0 if unsupported.
+ */
+ public long on(PwleSegment[] primitives, long vibrationId) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE v2)");
+ try {
+ if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)) {
+ return 0;
+ }
+ synchronized (mLock) {
+ long duration = mNativeWrapper.composePwleV2(primitives, vibrationId);
+ if (duration > 0) {
+ mCurrentAmplitude = -1;
+ updateStateAndNotifyListenersLocked(VibratorState.VIBRATING);
+ }
+ return duration;
+ }
+ } finally {
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
+ }
+ }
+
+ /**
* Turns off the vibrator and disables completion callback to any pending vibration.
*
* <p>This will affect the state of {@link #isVibrating()}.
@@ -534,6 +562,9 @@
private static native long performPwleEffect(long nativePtr, RampSegment[] effect,
int braking, long vibrationId);
+ private static native long performPwleV2Effect(long nativePtr, PwleSegment[] effect,
+ long vibrationId);
+
private static native void setExternalControl(long nativePtr, boolean enabled);
private static native void alwaysOnEnable(long nativePtr, long id, long effect,
@@ -600,6 +631,11 @@
return performPwleEffect(mNativePtr, primitives, braking, vibrationId);
}
+ /** Turns vibrator on to perform PWLE effect composed of given primitives. */
+ public long composePwleV2(PwleSegment[] primitives, long vibrationId) {
+ return performPwleV2Effect(mNativePtr, primitives, vibrationId);
+ }
+
/** Enabled the device vibrator to be controlled by another service. */
public void setExternalControl(boolean enabled) {
setExternalControl(mNativePtr, enabled);
diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS
index 5d6d8bc..e983edf 100644
--- a/services/core/java/com/android/server/wm/OWNERS
+++ b/services/core/java/com/android/server/wm/OWNERS
@@ -32,3 +32,7 @@
# Files related to tracing
per-file *TransitionTracer.java = file:platform/development:/tools/winscope/OWNERS
per-file *WindowTracing* = file:platform/development:/tools/winscope/OWNERS
+
+# Files related to activity security
+per-file ActivityStarter.java = file:/ACTIVITY_SECURITY_OWNERS
+per-file ActivityTaskManagerService.java = file:/ACTIVITY_SECURITY_OWNERS
diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
index 903d892..59dbf28 100644
--- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp
+++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp
@@ -73,6 +73,13 @@
jfieldID endFrequencyHz;
jfieldID duration;
} sRampClassInfo;
+static struct {
+ jfieldID startAmplitude;
+ jfieldID endAmplitude;
+ jfieldID startFrequencyHz;
+ jfieldID endFrequencyHz;
+ jfieldID duration;
+} sPwleClassInfo;
static_assert(static_cast<uint8_t>(V1_0::EffectStrength::LIGHT) ==
static_cast<uint8_t>(Aidl::EffectStrength::LIGHT));
@@ -182,6 +189,15 @@
return pwle;
}
+static Aidl::PwleV2Primitive pwleV2PrimitiveFromJavaPrimitive(JNIEnv* env, jobject pwleObj) {
+ Aidl::PwleV2Primitive pwle;
+ pwle.amplitude = static_cast<float>(env->GetFloatField(pwleObj, sPwleClassInfo.endAmplitude));
+ pwle.frequencyHz =
+ static_cast<float>(env->GetFloatField(pwleObj, sPwleClassInfo.endFrequencyHz));
+ pwle.timeMillis = static_cast<int32_t>(env->GetIntField(pwleObj, sPwleClassInfo.duration));
+ return pwle;
+}
+
/* Return true if braking is not NONE and the active PWLE starts and ends with zero amplitude. */
static bool shouldBeReplacedWithBraking(Aidl::ActivePwle activePwle, Aidl::Braking braking) {
return (braking != Aidl::Braking::NONE) && (activePwle.startAmplitude == 0) &&
@@ -399,6 +415,31 @@
return result.isOk() ? totalDuration.count() : (result.isUnsupported() ? 0 : -1);
}
+static jlong vibratorPerformPwleV2Effect(JNIEnv* env, jclass /* clazz */, jlong ptr,
+ jobjectArray waveform, jlong vibrationId) {
+ VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
+ if (wrapper == nullptr) {
+ ALOGE("vibratorPerformPwleV2Effect failed because native wrapper was not initialized");
+ return -1;
+ }
+ size_t size = env->GetArrayLength(waveform);
+ Aidl::CompositePwleV2 composite;
+ std::vector<Aidl::PwleV2Primitive> primitives;
+ for (size_t i = 0; i < size; i++) {
+ jobject element = env->GetObjectArrayElement(waveform, i);
+ Aidl::PwleV2Primitive pwle = pwleV2PrimitiveFromJavaPrimitive(env, element);
+ primitives.push_back(pwle);
+ }
+ composite.pwlePrimitives = primitives;
+
+ auto callback = wrapper->createCallback(vibrationId);
+ auto composePwleV2Fn = [&composite, &callback](vibrator::HalWrapper* hal) {
+ return hal->composePwleV2(composite, callback);
+ };
+ auto result = wrapper->halCall<void>(composePwleV2Fn, "composePwleV2");
+ return result.isOk();
+}
+
static void vibratorAlwaysOnEnable(JNIEnv* env, jclass /* clazz */, jlong ptr, jlong id,
jlong effect, jlong strength) {
VibratorControllerWrapper* wrapper = reinterpret_cast<VibratorControllerWrapper*>(ptr);
@@ -579,6 +620,8 @@
(void*)vibratorPerformComposedEffect},
{"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJ)J",
(void*)vibratorPerformPwleEffect},
+ {"performPwleV2Effect", "(J[Landroid/os/vibrator/PwleSegment;J)J",
+ (void*)vibratorPerformPwleV2Effect},
{"setExternalControl", "(JZ)V", (void*)vibratorSetExternalControl},
{"alwaysOnEnable", "(JJJJ)V", (void*)vibratorAlwaysOnEnable},
{"alwaysOnDisable", "(JJ)V", (void*)vibratorAlwaysOnDisable},
@@ -604,6 +647,13 @@
sRampClassInfo.endFrequencyHz = GetFieldIDOrDie(env, rampClass, "mEndFrequencyHz", "F");
sRampClassInfo.duration = GetFieldIDOrDie(env, rampClass, "mDuration", "I");
+ jclass pwleClass = FindClassOrDie(env, "android/os/vibrator/PwleSegment");
+ sPwleClassInfo.startAmplitude = GetFieldIDOrDie(env, pwleClass, "mStartAmplitude", "F");
+ sPwleClassInfo.endAmplitude = GetFieldIDOrDie(env, pwleClass, "mEndAmplitude", "F");
+ sPwleClassInfo.startFrequencyHz = GetFieldIDOrDie(env, pwleClass, "mStartFrequencyHz", "F");
+ sPwleClassInfo.endFrequencyHz = GetFieldIDOrDie(env, pwleClass, "mEndFrequencyHz", "F");
+ sPwleClassInfo.duration = GetFieldIDOrDie(env, pwleClass, "mDuration", "I");
+
jclass frequencyProfileLegacyClass =
FindClassOrDie(env, "android/os/VibratorInfo$FrequencyProfileLegacy");
sFrequencyProfileLegacyClass =
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 6baab25..7eb0c42 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -28,6 +28,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.server.utils.TimingsTraceAndSlog.SYSTEM_SERVER_TIMING_TAG;
+import static com.android.tradeinmode.flags.Flags.enableTradeInMode;
import android.annotation.NonNull;
import android.annotation.StringRes;
@@ -1399,10 +1400,6 @@
mSystemServiceManager.startService(BatteryService.class);
t.traceEnd();
- t.traceBegin("StartTradeInModeService");
- mSystemServiceManager.startService(TradeInModeService.class);
- t.traceEnd();
-
// Tracks application usage stats.
t.traceBegin("StartUsageService");
mSystemServiceManager.startService(UsageStatsService.class);
@@ -1772,6 +1769,13 @@
mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class);
t.traceEnd();
}
+
+ if (!isWatch && !isTv && !isAutomotive && enableTradeInMode()) {
+ t.traceBegin("StartTradeInModeService");
+ mSystemServiceManager.startService(TradeInModeService.class);
+ t.traceEnd();
+ }
+
} catch (Throwable e) {
Slog.e("System", "******************************************");
Slog.e("System", "************ Failure starting core service");
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 2bc8af1..5bb6b19 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -85,6 +85,8 @@
private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
"settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
private Instrumentation mInstrumentation;
private UiDevice mUiDevice;
private Context mContext;
@@ -95,7 +97,7 @@
private boolean mShowImeWithHardKeyboardEnabled;
@Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
@Before
public void setUp() throws Exception {
@@ -159,7 +161,7 @@
// Press home key to hide soft keyboard.
Log.i(TAG, "Press home");
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
assertThat(mUiDevice.pressHome()).isTrue();
// The IME visibility is only sent at the end of the animation. Therefore, we have to
// wait until the visibility was sent to the server and the IME window hidden.
@@ -774,7 +776,7 @@
backButtonUiObject.click();
mInstrumentation.waitForIdleSync();
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
// The IME visibility is only sent at the end of the animation. Therefore, we have to
// wait until the visibility was sent to the server and the IME window hidden.
eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
@@ -812,7 +814,7 @@
backButtonUiObject.longClick();
mInstrumentation.waitForIdleSync();
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
// The IME visibility is only sent at the end of the animation. Therefore, we have to
// wait until the visibility was sent to the server and the IME window hidden.
eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
@@ -900,7 +902,7 @@
assertWithMessage("Input Method Switcher Menu is shown")
.that(isInputMethodPickerShown(imm))
.isTrue();
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
// The IME visibility is only sent at the end of the animation. Therefore, we have to
// wait until the visibility was sent to the server and the IME window hidden.
eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 6afcae7..3aeab09 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -76,8 +76,10 @@
@RunWith(AndroidJUnit4.class)
public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTestBase {
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
@Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
private DefaultImeVisibilityApplier mVisibilityApplier;
@Before
@@ -151,7 +153,7 @@
mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
STATE_HIDE_IME_EXPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId);
}
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
verifySetImeVisibility(false /* setVisible */, true /* invoked */);
} else {
@@ -168,7 +170,7 @@
mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
STATE_HIDE_IME_NOT_ALWAYS, eq(SoftInputShowHideReason.NOT_SET), mUserId);
}
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
verifySetImeVisibility(false /* setVisible */, true /* invoked */);
} else {
@@ -182,7 +184,7 @@
mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
STATE_SHOW_IME_IMPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId);
}
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, true /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -260,7 +262,7 @@
verify(mVisibilityApplier).applyImeVisibility(
eq(mWindowToken), any(), eq(STATE_HIDE_IME),
eq(SoftInputShowHideReason.NOT_SET), eq(mUserId) /* userId */);
- if (!Flags.refactorInsetsController()) {
+ if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verify(mInputMethodManagerService.mWindowManagerInternal).hideIme(eq(mWindowToken),
eq(displayIdToShowIme), and(not(eq(statsToken)), notNull()));
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index 4d956b2..c958bd3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -39,8 +39,10 @@
import android.os.IBinder;
import android.os.LocaleList;
import android.os.RemoteException;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.Flags;
import android.window.ImeOnBackInvokedDispatcher;
import com.android.internal.inputmethod.IInputMethodClient;
@@ -89,6 +91,9 @@
};
private static final int DEFAULT_SOFT_INPUT_FLAG =
StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR;
+
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
@Mock
VirtualDeviceManagerInternal mMockVdmInternal;
@@ -125,7 +130,7 @@
case SOFT_INPUT_STATE_UNSPECIFIED:
boolean showSoftInput =
(mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE) || mIsLargeScreen;
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, showSoftInput /* invoked */);
// A hide can only be triggered if there is no editorFocused, which this test
// always sets.
@@ -141,7 +146,7 @@
break;
case SOFT_INPUT_STATE_VISIBLE:
case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, true /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -150,7 +155,7 @@
}
break;
case SOFT_INPUT_STATE_UNCHANGED:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -160,7 +165,7 @@
break;
case SOFT_INPUT_STATE_HIDDEN:
case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
// In this case, we don't have to manipulate the requested visible types of
// the WindowState, as they're already in the correct state
@@ -192,7 +197,7 @@
case SOFT_INPUT_STATE_UNSPECIFIED:
boolean hideSoftInput =
(mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE) && !mIsLargeScreen;
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
// A show can only be triggered in forward navigation
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
// A hide can only be triggered if there is no editorFocused, which this test
@@ -209,7 +214,7 @@
case SOFT_INPUT_STATE_VISIBLE:
case SOFT_INPUT_STATE_HIDDEN:
case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -218,7 +223,7 @@
}
break;
case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, true /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -227,7 +232,7 @@
}
break;
case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
// In this case, we don't have to manipulate the requested visible types of
// the WindowState, as they're already in the correct state
diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
index 5c4716d..7d5532f 100644
--- a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
+++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
@@ -57,6 +57,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doReturn
@@ -383,6 +384,10 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
PackageManager.PERMISSION_GRANTED
}
+ whenever(this.checkPermission(
+ eq(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL), anyInt(), anyInt())) {
+ PackageManager.PERMISSION_GRANTED
+ }
}
val mockSharedLibrariesImpl: SharedLibrariesImpl = mock {
whenever(this.snapshot()) { this@mock }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index b2fe138..d66bb00 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -1692,16 +1692,9 @@
3 * MINUTE_IN_MILLIS, 5), false);
final long timeUntilQuotaConsumedMs = 7 * MINUTE_IN_MILLIS;
JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked", 0);
- //noinspection deprecation
- JobStatus jobDefIWF = createJobStatus("testGetMaxJobExecutionTimeLocked",
- createJobInfoBuilder(1)
- .setImportantWhileForeground(true)
- .setPriority(JobInfo.PRIORITY_DEFAULT)
- .build());
JobStatus jobHigh = createJobStatus("testGetMaxJobExecutionTimeLocked",
createJobInfoBuilder(2).setPriority(JobInfo.PRIORITY_HIGH).build());
setStandbyBucket(RARE_INDEX, job);
- setStandbyBucket(RARE_INDEX, jobDefIWF);
setStandbyBucket(RARE_INDEX, jobHigh);
setCharging();
@@ -1709,8 +1702,6 @@
assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
- mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
- assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh)));
}
@@ -1720,8 +1711,6 @@
assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
- mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
- assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh)));
}
@@ -1730,9 +1719,8 @@
// Top-stared jobs are out of quota enforcement.
setProcessState(ActivityManager.PROCESS_STATE_TOP);
synchronized (mQuotaController.mLock) {
- trackJobs(job, jobDefIWF, jobHigh);
+ trackJobs(job, jobHigh);
mQuotaController.prepareForExecutionLocked(job);
- mQuotaController.prepareForExecutionLocked(jobDefIWF);
mQuotaController.prepareForExecutionLocked(jobHigh);
}
setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
@@ -1740,11 +1728,8 @@
assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
- mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
- assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh)));
mQuotaController.maybeStopTrackingJobLocked(job, null);
- mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
mQuotaController.maybeStopTrackingJobLocked(jobHigh, null);
}
@@ -1753,8 +1738,6 @@
assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked(job));
assertEquals(timeUntilQuotaConsumedMs,
- mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
- assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked(jobHigh));
}
@@ -1762,9 +1745,8 @@
// Quota is enforced for top-started job after the process leaves TOP/BTOP state.
setProcessState(ActivityManager.PROCESS_STATE_TOP);
synchronized (mQuotaController.mLock) {
- trackJobs(job, jobDefIWF, jobHigh);
+ trackJobs(job, jobHigh);
mQuotaController.prepareForExecutionLocked(job);
- mQuotaController.prepareForExecutionLocked(jobDefIWF);
mQuotaController.prepareForExecutionLocked(jobHigh);
}
setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
@@ -1772,11 +1754,8 @@
assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
assertEquals(timeUntilQuotaConsumedMs,
- mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
- assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked((jobHigh)));
mQuotaController.maybeStopTrackingJobLocked(job, null);
- mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
mQuotaController.maybeStopTrackingJobLocked(jobHigh, null);
}
@@ -1785,13 +1764,145 @@
assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked(job));
assertEquals(timeUntilQuotaConsumedMs,
- mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
- assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked(jobHigh));
}
}
@Test
+ public void testGetMaxJobExecutionTimeLocked_Regular_ImportantWhileForeground() {
+ mQuotaController.saveTimingSession(0, SOURCE_PACKAGE,
+ createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS),
+ 3 * MINUTE_IN_MILLIS, 5), false);
+ final long timeUntilQuotaConsumedMs = 7 * MINUTE_IN_MILLIS;
+ JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked", 0);
+ //noinspection deprecation
+ JobStatus jobDefIWF;
+ mSetFlagsRule.disableFlags(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND);
+ jobDefIWF = createJobStatus("testGetMaxJobExecutionTimeLocked_IWF",
+ createJobInfoBuilder(1)
+ .setImportantWhileForeground(true)
+ .setPriority(JobInfo.PRIORITY_DEFAULT)
+ .build());
+
+ setStandbyBucket(RARE_INDEX, jobDefIWF);
+ setCharging();
+ synchronized (mQuotaController.mLock) {
+ assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+ }
+
+ setDischarging();
+ setProcessState(getProcessStateQuotaFreeThreshold());
+ synchronized (mQuotaController.mLock) {
+ assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+ }
+
+ // Top-started job
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS);
+ // Top-stared jobs are out of quota enforcement.
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ trackJobs(jobDefIWF);
+ mQuotaController.prepareForExecutionLocked(jobDefIWF);
+ }
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+ mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
+ }
+
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(timeUntilQuotaConsumedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
+ }
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS);
+ // Quota is enforced for top-started job after the process leaves TOP/BTOP state.
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ trackJobs(jobDefIWF);
+ mQuotaController.prepareForExecutionLocked(jobDefIWF);
+ }
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(timeUntilQuotaConsumedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+ mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
+ }
+
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(timeUntilQuotaConsumedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
+ }
+
+ mSetFlagsRule.enableFlags(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND);
+ jobDefIWF = createJobStatus("testGetMaxJobExecutionTimeLocked_IWF",
+ createJobInfoBuilder(1)
+ .setImportantWhileForeground(true)
+ .setPriority(JobInfo.PRIORITY_DEFAULT)
+ .build());
+
+ setStandbyBucket(RARE_INDEX, jobDefIWF);
+ setCharging();
+ synchronized (mQuotaController.mLock) {
+ assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+ mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+ }
+
+ setDischarging();
+ setProcessState(getProcessStateQuotaFreeThreshold());
+ synchronized (mQuotaController.mLock) {
+ assertEquals(timeUntilQuotaConsumedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+ }
+
+ // Top-started job
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS);
+ // Top-stared jobs are out of quota enforcement.
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ trackJobs(jobDefIWF);
+ mQuotaController.prepareForExecutionLocked(jobDefIWF);
+ }
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(timeUntilQuotaConsumedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+ mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
+ }
+
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(timeUntilQuotaConsumedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
+ }
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS);
+ // Quota is enforced for top-started job after the process leaves TOP/BTOP state.
+ setProcessState(ActivityManager.PROCESS_STATE_TOP);
+ synchronized (mQuotaController.mLock) {
+ trackJobs(jobDefIWF);
+ mQuotaController.prepareForExecutionLocked(jobDefIWF);
+ }
+ setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(timeUntilQuotaConsumedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked((jobDefIWF)));
+ mQuotaController.maybeStopTrackingJobLocked(jobDefIWF, null);
+ }
+
+ setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(timeUntilQuotaConsumedMs,
+ mQuotaController.getMaxJobExecutionTimeMsLocked(jobDefIWF));
+ }
+ }
+
+ @Test
public void testGetMaxJobExecutionTimeLocked_Regular_Active() {
JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked_Regular_Active", 0);
setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index b4b3612..bc410d9 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -1580,12 +1580,12 @@
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
- invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS));
+ invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK));
when(mTrustManager.isInSignificantPlace()).thenReturn(true);
- assertEquals(BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE,
- invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS));
+ assertEquals(BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE,
+ invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK));
}
@Test
@@ -1603,13 +1603,13 @@
setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG);
assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
- invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS
+ invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK
| Authenticators.BIOMETRIC_STRONG));
when(mTrustManager.isInSignificantPlace()).thenReturn(true);
assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
- invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS
+ invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK
| Authenticators.BIOMETRIC_STRONG));
}
@@ -1628,12 +1628,12 @@
setupAuthForOnly(TYPE_CREDENTIAL, Authenticators.DEVICE_CREDENTIAL);
assertEquals(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
- invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS));
+ invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK));
when(mTrustManager.isInSignificantPlace()).thenReturn(true);
assertEquals(BiometricManager.BIOMETRIC_SUCCESS,
- invokeCanAuthenticate(mBiometricService, Authenticators.MANDATORY_BIOMETRICS
+ invokeCanAuthenticate(mBiometricService, Authenticators.IDENTITY_CHECK
| Authenticators.DEVICE_CREDENTIAL));
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
index b758f57..85e45f4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java
@@ -207,7 +207,7 @@
final BiometricSensor sensor = getFaceSensor();
final PromptInfo promptInfo = new PromptInfo();
- promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+ promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
@@ -222,7 +222,7 @@
when(mTrustManager.isInSignificantPlace()).thenReturn(false);
final PromptInfo promptInfo = new PromptInfo();
- promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+ promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
mSettingObserver, List.of(), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
@@ -238,7 +238,7 @@
final BiometricSensor sensor = getFaceSensor();
final PromptInfo promptInfo = new PromptInfo();
- promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS
+ promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK
| BiometricManager.Authenticators.BIOMETRIC_STRONG);
final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
@@ -255,13 +255,13 @@
final BiometricSensor sensor = getFaceSensor();
final PromptInfo promptInfo = new PromptInfo();
- promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+ promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager);
assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo(
- BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE);
+ BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE);
assertThat(preAuthInfo.eligibleSensors).hasSize(0);
}
@@ -296,7 +296,7 @@
final BiometricSensor sensor = getFaceSensor();
final PromptInfo promptInfo = new PromptInfo();
- promptInfo.setAuthenticators(BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
+ promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK);
promptInfo.setNegativeButtonText(TEST_PACKAGE_NAME);
final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
index 1bea371..c4167d2 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
@@ -212,24 +212,24 @@
mContext, Authenticators.BIOMETRIC_MIN_STRENGTH));
assertThrows(SecurityException.class, () -> Utils.isValidAuthenticatorConfig(
- mContext, Authenticators.MANDATORY_BIOMETRICS));
+ mContext, Authenticators.IDENTITY_CHECK));
doNothing().when(mContext).enforceCallingOrSelfPermission(
eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
if (Flags.mandatoryBiometrics()) {
assertTrue(Utils.isValidAuthenticatorConfig(mContext,
- Authenticators.MANDATORY_BIOMETRICS));
+ Authenticators.IDENTITY_CHECK));
} else {
assertFalse(Utils.isValidAuthenticatorConfig(mContext,
- Authenticators.MANDATORY_BIOMETRICS));
+ Authenticators.IDENTITY_CHECK));
}
// The rest of the bits are not allowed to integrate with the public APIs
for (int i = 8; i < 32; i++) {
final int authenticator = 1 << i;
if (authenticator == Authenticators.DEVICE_CREDENTIAL
- || authenticator == Authenticators.MANDATORY_BIOMETRICS) {
+ || authenticator == Authenticators.IDENTITY_CHECK) {
continue;
}
assertFalse(Utils.isValidAuthenticatorConfig(mContext, 1 << i));
@@ -307,8 +307,8 @@
BiometricManager.BIOMETRIC_ERROR_LOCKOUT},
{BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT,
BiometricManager.BIOMETRIC_ERROR_LOCKOUT},
- {BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE,
- BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE}
+ {BiometricConstants.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE,
+ BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE}
};
for (int i = 0; i < testCases.length; i++) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 1331ae1..b110ff6 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -160,6 +160,7 @@
// Make a possibly-not-full-permission (i.e. partial) copy and check that it is correct.
final UserProperties copy = new UserProperties(orig, exposeAll, hasManage, hasQuery);
+ assertThat(copy.toString()).isNotNull();
verifyTestCopyLacksPermissions(orig, copy, exposeAll, hasManage, hasQuery);
if (permLevel < 1) {
// PropertiesPresent should definitely be different since not all items were copied.
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
index 5de323b..4d82c3c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
@@ -209,18 +209,30 @@
}
@Test
- public void getShortDaysSummary_onlyDays() {
+ public void getDaysOfWeekShort_summarizesDays() {
ScheduleInfo scheduleInfo = new ScheduleInfo();
scheduleInfo.startHour = 10;
scheduleInfo.endHour = 16;
scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
- assertThat(SystemZenRules.getShortDaysSummary(mContext, scheduleInfo))
+ assertThat(SystemZenRules.getDaysOfWeekShort(mContext, scheduleInfo))
.isEqualTo("Mon-Fri");
}
@Test
+ public void getDaysOfWeekFull_summarizesDays() {
+ ScheduleInfo scheduleInfo = new ScheduleInfo();
+ scheduleInfo.startHour = 10;
+ scheduleInfo.endHour = 16;
+ scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
+ Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
+
+ assertThat(SystemZenRules.getDaysOfWeekFull(mContext, scheduleInfo))
+ .isEqualTo("Monday to Friday");
+ }
+
+ @Test
public void getTimeSummary_onlyTime() {
ScheduleInfo scheduleInfo = new ScheduleInfo();
scheduleInfo.startHour = 11;
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
index d7ae046..88ed615 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
@@ -29,8 +29,10 @@
import android.os.PersistableBundle;
import android.os.VibrationEffect;
import android.os.test.TestLooper;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationConfig;
@@ -57,11 +59,20 @@
private static final int EMPTY_VIBRATOR_ID = 1;
private static final int PWLE_VIBRATOR_ID = 2;
private static final int PWLE_WITHOUT_FREQUENCIES_VIBRATOR_ID = 3;
+ private static final int PWLE_V2_VIBRATOR_ID = 4;
private static final float TEST_MIN_FREQUENCY = 50;
private static final float TEST_RESONANT_FREQUENCY = 150;
private static final float TEST_FREQUENCY_RESOLUTION = 25;
private static final float[] TEST_AMPLITUDE_MAP = new float[]{
/* 50Hz= */ 0.08f, 0.16f, 0.32f, 0.64f, /* 150Hz= */ 0.8f, 0.72f, /* 200Hz= */ 0.64f};
+ private static final int TEST_MAX_ENVELOPE_EFFECT_SIZE = 10;
+ private static final int TEST_MIN_ENVELOPE_EFFECT_CONTROL_POINT_DURATION_MILLIS = 20;
+ private static final float[] TEST_FREQUENCIES_HZ = new float[]{30f, 50f, 100f, 120f, 150f};
+ private static final float[] TEST_OUTPUT_ACCELERATIONS_GS =
+ new float[]{0.3f, 0.5f, 1.0f, 0.8f, 0.6f};
+ private static final float PWLE_V2_MIN_FREQUENCY = TEST_FREQUENCIES_HZ[0];
+ private static final float PWLE_V2_MAX_FREQUENCY =
+ TEST_FREQUENCIES_HZ[TEST_FREQUENCIES_HZ.length - 1];
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@@ -296,6 +307,77 @@
assertThat(vibration.adapt(mAdapter)).isEqualTo(expected);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testPwleSegment_withoutPwleV2Capability_returnsNull() {
+ VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+ new PrimitiveSegment(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.5f, 100),
+ new PwleSegment(1, 0.2f, 30, 60, 20),
+ new PwleSegment(0.8f, 0.2f, 60, 100, 100),
+ new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
+ /* repeatIndex= */ 1);
+
+ VibrationEffect.Composed adaptedEffect =
+ (VibrationEffect.Composed) mAdapter.adaptToVibrator(EMPTY_VIBRATOR_ID, effect);
+ assertThat(adaptedEffect).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testPwleSegment_withPwleV2Capability_returnsAdaptedSegments() {
+ VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+ new PwleSegment(1, 0.2f, 30, 60, 20),
+ new PwleSegment(0.8f, 0.2f, 60, 100, 100),
+ new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
+ /* repeatIndex= */ 1);
+
+ VibrationEffect.Composed expected = new VibrationEffect.Composed(Arrays.asList(
+ new PwleSegment(1, 0.2f, 30, 60, 20),
+ new PwleSegment(0.8f, 0.2f, 60, 100, 100),
+ new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
+ /* repeatIndex= */ 1);
+
+ SparseArray<VibratorController> vibrators = new SparseArray<>();
+ vibrators.put(PWLE_V2_VIBRATOR_ID, createPwleV2VibratorController(PWLE_V2_VIBRATOR_ID));
+ DeviceAdapter adapter = new DeviceAdapter(mVibrationSettings, vibrators);
+
+ assertThat(adapter.adaptToVibrator(PWLE_V2_VIBRATOR_ID, effect)).isEqualTo(expected);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testPwleSegment_withFrequenciesBelowSupportedRange_returnsNull() {
+ float frequencyBelowSupportedRange = PWLE_V2_MIN_FREQUENCY - 1f;
+ VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+ new PwleSegment(0, 0.2f, 30, 60, 20),
+ new PwleSegment(0.8f, 0.2f, 60, frequencyBelowSupportedRange, 100),
+ new PwleSegment(0.65f, 0.65f, frequencyBelowSupportedRange, 50, 50)),
+ /* repeatIndex= */ 1);
+
+ SparseArray<VibratorController> vibrators = new SparseArray<>();
+ vibrators.put(PWLE_V2_VIBRATOR_ID, createPwleV2VibratorController(PWLE_V2_VIBRATOR_ID));
+ DeviceAdapter adapter = new DeviceAdapter(mVibrationSettings, vibrators);
+
+ assertThat(adapter.adaptToVibrator(PWLE_V2_VIBRATOR_ID, effect)).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testPwleSegment_withFrequenciesAboveSupportedRange_returnsNull() {
+ float frequencyAboveSupportedRange = PWLE_V2_MAX_FREQUENCY + 1f;
+ VibrationEffect.Composed effect = new VibrationEffect.Composed(Arrays.asList(
+ new PwleSegment(0, 0.2f, 30, frequencyAboveSupportedRange, 20),
+ new PwleSegment(0.8f, 0.2f, frequencyAboveSupportedRange, 100, 100),
+ new PwleSegment(0.65f, 0.65f, 100, 50, 50)),
+ /* repeatIndex= */ 1);
+
+ SparseArray<VibratorController> vibrators = new SparseArray<>();
+ vibrators.put(PWLE_V2_VIBRATOR_ID, createPwleV2VibratorController(PWLE_V2_VIBRATOR_ID));
+ DeviceAdapter adapter = new DeviceAdapter(mVibrationSettings, vibrators);
+
+ assertThat(adapter.adaptToVibrator(PWLE_V2_VIBRATOR_ID, effect)).isNull();
+ }
+
private VibratorController createEmptyVibratorController(int vibratorId) {
return new FakeVibratorControllerProvider(mTestLooper.getLooper())
.newVibratorController(vibratorId, (id, vibrationId) -> {});
@@ -318,4 +400,18 @@
provider.setMaxAmplitudes(TEST_AMPLITUDE_MAP);
return provider.newVibratorController(vibratorId, (id, vibrationId) -> {});
}
+
+ private VibratorController createPwleV2VibratorController(int vibratorId) {
+ FakeVibratorControllerProvider provider = new FakeVibratorControllerProvider(
+ mTestLooper.getLooper());
+ provider.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+ provider.setResonantFrequency(TEST_RESONANT_FREQUENCY);
+ provider.setFrequenciesHz(TEST_FREQUENCIES_HZ);
+ provider.setOutputAccelerationsGs(TEST_OUTPUT_ACCELERATIONS_GS);
+ provider.setMaxEnvelopeEffectSize(TEST_MAX_ENVELOPE_EFFECT_SIZE);
+ provider.setMinEnvelopeEffectControlPointDurationMillis(
+ TEST_MIN_ENVELOPE_EFFECT_CONTROL_POINT_DURATION_MILLIS);
+
+ return provider.newVibratorController(vibratorId, (id, vibrationId) -> {});
+ }
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 58a1e84..8aa8a84 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -59,11 +59,13 @@
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -875,6 +877,49 @@
}
@Test
+ @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void vibrate_singleVibratorPwle_runsComposePwleV2() {
+ FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+ fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+ fakeVibrator.setResonantFrequency(150);
+ fakeVibrator.setFrequenciesHz(new float[]{30f, 50f, 100f, 120f, 150f});
+ fakeVibrator.setOutputAccelerationsGs(new float[]{0.3f, 0.5f, 1.0f, 0.8f, 0.6f});
+ fakeVibrator.setMaxEnvelopeEffectSize(10);
+ fakeVibrator.setMinEnvelopeEffectControlPointDurationMillis(20);
+
+ VibrationEffect effect = VibrationEffect.startWaveformEnvelope()
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20)
+ .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 30)
+ .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 20)
+ .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 30)
+ .build();
+ HalVibration vibration = startThreadAndDispatcher(effect);
+ waitForCompletion();
+
+ verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L));
+ verify(mManagerHooks).noteVibratorOff(eq(UID));
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
+ assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
+ assertEquals(Arrays.asList(
+ expectedPwle(/* startAmplitude= */ 0.0f, /* endAmplitude= */ 0.0f,
+ /* startFrequencyHz= */ 60f, /* endFrequencyHz= */ 60f,
+ /* duration= */ 20),
+ expectedPwle(/* startAmplitude= */ 0.0f, /* endAmplitude= */ 0.3f,
+ /* startFrequencyHz= */ 60f, /* endFrequencyHz= */ 100f,
+ /* duration= */ 30),
+ expectedPwle(/* startAmplitude= */ 0.3f, /* endAmplitude= */ 0.4f,
+ /* startFrequencyHz= */ 100f, /* endFrequencyHz= */ 120f,
+ /* duration= */ 20),
+ expectedPwle(/* startAmplitude= */ 0.4f, /* endAmplitude= */ 0.0f,
+ /* startFrequencyHz= */ 120f, /* endFrequencyHz= */ 120f,
+ /* duration= */ 30)
+ ),
+ fakeVibrator.getEffectSegments(vibration.id));
+
+ }
+
+ @Test
@DisableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
public void vibrate_singleVibratorPwle_runsComposePwle() {
FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
@@ -1968,6 +2013,12 @@
duration);
}
+ private VibrationEffectSegment expectedPwle(float startAmplitude, float endAmplitude,
+ float startFrequencyHz, float endFrequencyHz, int duration) {
+ return new PwleSegment(startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz,
+ duration);
+ }
+
private List<Float> expectedAmplitudes(int... amplitudes) {
return Arrays.stream(amplitudes)
.mapToObj(amplitude -> amplitude / 255f)
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
index 8179369..bc8db3b 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java
@@ -44,6 +44,7 @@
import android.os.test.TestLooper;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import androidx.test.InstrumentationRegistry;
@@ -268,6 +269,22 @@
}
@Test
+ public void on_withComposedPwleV2_performsEffect() {
+ mockVibratorCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2);
+ when(mNativeWrapperMock.composePwleV2(any(), anyLong())).thenReturn(15L);
+ VibratorController controller = createController();
+
+ PwleSegment[] primitives = new PwleSegment[]{
+ new PwleSegment(/* startAmplitude= */ 0, /* endAmplitude= */ 1,
+ /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 200, /* duration= */ 10)
+ };
+ assertEquals(15L, controller.on(primitives, 12));
+ assertTrue(controller.isVibrating());
+
+ verify(mNativeWrapperMock).composePwleV2(eq(primitives), eq(12L));
+ }
+
+ @Test
public void off_turnsOffVibrator() {
when(mNativeWrapperMock.on(anyLong(), anyLong())).thenAnswer(args -> args.getArgument(0));
VibratorController controller = createController();
diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
index 75a9cedf..2c3e9b2 100644
--- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -26,6 +26,7 @@
import android.os.VibratorInfo;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
@@ -194,6 +195,19 @@
}
@Override
+ public long composePwleV2(PwleSegment[] primitives, long vibrationId) {
+ long duration = 0;
+ for (PwleSegment primitive: primitives) {
+ duration += primitive.getDuration();
+ recordEffectSegment(vibrationId, primitive);
+ }
+ applyLatency(mOnLatency);
+ scheduleListener(duration, vibrationId);
+
+ return duration;
+ }
+
+ @Override
public void setExternalControl(boolean enabled) {
mExternalControlStates.add(enabled);
}
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 4ac567c..1c6bd11 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -109,11 +109,7 @@
if (motionEventHelper.inputMethod == TOUCH
&& Flags.enableHoldToDragAppHandle()) {
// Touch requires hold-to-drag.
- val downTime = SystemClock.uptimeMillis()
- motionEventHelper.actionDown(startX, startY, time = downTime)
- SystemClock.sleep(100L) // hold for 100ns before starting the move.
- motionEventHelper.actionMove(startX, startY, startX, endY, 100, downTime = downTime)
- motionEventHelper.actionUp(startX, endY, downTime = downTime)
+ motionEventHelper.holdToDrag(startX, startY, startX, endY, steps = 100)
} else {
device.drag(startX, startY, startX, endY, 100)
}
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
index 86a0b0f..1fe6088 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
@@ -54,7 +54,15 @@
injectMotionEvent(ACTION_UP, x, y, downTime = downTime)
}
- fun actionMove(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int, downTime: Long) {
+ fun actionMove(
+ startX: Int,
+ startY: Int,
+ endX: Int,
+ endY: Int,
+ steps: Int,
+ downTime: Long,
+ withMotionEventInjectDelay: Boolean = false
+ ) {
val incrementX = (endX - startX).toFloat() / (steps - 1)
val incrementY = (endY - startY).toFloat() / (steps - 1)
@@ -65,9 +73,33 @@
val moveEvent = getMotionEvent(downTime, time, ACTION_MOVE, x, y)
injectMotionEvent(moveEvent)
+ if (withMotionEventInjectDelay) {
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS)
+ }
}
}
+ /**
+ * Drag from [startX], [startY] to [endX], [endY] with a "hold" period after touching down
+ * and before moving.
+ */
+ fun holdToDrag(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int) {
+ val downTime = SystemClock.uptimeMillis()
+ actionDown(startX, startY, time = downTime)
+ SystemClock.sleep(100L) // Hold before dragging.
+ actionMove(
+ startX,
+ startY,
+ endX,
+ endY,
+ steps,
+ downTime,
+ withMotionEventInjectDelay = true
+ )
+ SystemClock.sleep(REGULAR_CLICK_LENGTH)
+ actionUp(startX, endX, downTime)
+ }
+
private fun injectMotionEvent(
action: Int,
x: Int,
@@ -120,4 +152,9 @@
event.displayId = 0
return event
}
+
+ companion object {
+ private const val MOTION_EVENT_INJECTION_DELAY_MILLIS = 5L
+ private const val REGULAR_CLICK_LENGTH = 100L
+ }
}
\ No newline at end of file
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 498e431..232b402 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -1574,7 +1574,10 @@
// If the file path ends with .flata, .jar, .jack, or .zip the file is treated
// as ZIP archive and the files within are merged individually.
// Otherwise the file is processed on its own.
- bool MergePath(const std::string& path, bool override) {
+ bool MergePath(std::string path, bool override) {
+ if (path.size() > 2 && util::StartsWith(path, "'") && util::EndsWith(path, "'")) {
+ path = path.substr(1, path.size() - 2);
+ }
if (util::EndsWith(path, ".flata") || util::EndsWith(path, ".jar") ||
util::EndsWith(path, ".jack") || util::EndsWith(path, ".zip")) {
return MergeArchive(path, override);