Merge "Refactor and fix flaky tests in WindowOnBackInvokedDispatcherTest" into udc-dev am: f4f331d65a am: ba85d52709 am: 692a86e309
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23352458
Change-Id: I8e2a5529bca189bd14ac7517b1f74f19085eab4c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 70a2e53..aa947eb 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -3882,7 +3882,9 @@
* it will set up the dispatch to call {@link #onKeyUp} where the action
* will be performed; for earlier applications, it will perform the
* action immediately in on-down, as those versions of the platform
- * behaved.
+ * behaved. This implementation will also take care of {@link KeyEvent#KEYCODE_ESCAPE}
+ * by finishing the activity if it would be closed by touching outside
+ * of it.
*
* <p>Other additional default key handling may be performed
* if configured with {@link #setDefaultKeyMode}.
@@ -3904,6 +3906,11 @@
return true;
}
+ if (keyCode == KeyEvent.KEYCODE_ESCAPE && mWindow.shouldCloseOnTouchOutside()) {
+ event.startTracking();
+ return true;
+ }
+
if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {
return false;
} else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {
@@ -3999,6 +4006,15 @@
return true;
}
}
+
+ if (keyCode == KeyEvent.KEYCODE_ESCAPE
+ && mWindow.shouldCloseOnTouchOutside()
+ && event.isTracking()
+ && !event.isCanceled()) {
+ finish();
+ return true;
+ }
+
return false;
}
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index 411d157..4851279 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -694,12 +694,22 @@
*/
@Override
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
- if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE)
- && event.isTracking()
- && !event.isCanceled()
- && !WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
- onBackPressed();
- return true;
+ if (event.isTracking() && !event.isCanceled()) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
+ onBackPressed();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_ESCAPE:
+ if (mCancelable) {
+ cancel();
+ } else {
+ dismiss();
+ }
+ return true;
+ }
}
return false;
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 2eb6ca7..8d2394b 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -2833,12 +2833,14 @@
}
/**
- * Note all {@link Uri} that are referenced internally, with the expectation
- * that Uri permission grants will need to be issued to ensure the recipient
- * of this object is able to render its contents.
- *
- * @hide
- */
+ * Note all {@link Uri} that are referenced internally, with the expectation that Uri permission
+ * grants will need to be issued to ensure the recipient of this object is able to render its
+ * contents.
+ * See b/281044385 for more context and examples about what happens when this isn't done
+ * correctly.
+ *
+ * @hide
+ */
public void visitUris(@NonNull Consumer<Uri> visitor) {
if (publicVersion != null) {
publicVersion.visitUris(visitor);
@@ -2882,13 +2884,13 @@
ArrayList<Person> people = extras.getParcelableArrayList(EXTRA_PEOPLE_LIST, android.app.Person.class);
if (people != null && !people.isEmpty()) {
for (Person p : people) {
- visitor.accept(p.getIconUri());
+ p.visitUris(visitor);
}
}
final Person person = extras.getParcelable(EXTRA_MESSAGING_PERSON, Person.class);
if (person != null) {
- visitor.accept(person.getIconUri());
+ person.visitUris(visitor);
}
final RemoteInputHistoryItem[] history = extras.getParcelableArray(
@@ -2910,12 +2912,7 @@
if (!ArrayUtils.isEmpty(messages)) {
for (MessagingStyle.Message message : MessagingStyle.Message
.getMessagesFromBundleArray(messages)) {
- visitor.accept(message.getDataUri());
-
- Person senderPerson = message.getSenderPerson();
- if (senderPerson != null) {
- visitor.accept(senderPerson.getIconUri());
- }
+ message.visitUris(visitor);
}
}
@@ -2924,12 +2921,7 @@
if (!ArrayUtils.isEmpty(historic)) {
for (MessagingStyle.Message message : MessagingStyle.Message
.getMessagesFromBundleArray(historic)) {
- visitor.accept(message.getDataUri());
-
- Person senderPerson = message.getSenderPerson();
- if (senderPerson != null) {
- visitor.accept(senderPerson.getIconUri());
- }
+ message.visitUris(visitor);
}
}
@@ -2939,7 +2931,7 @@
if (isStyle(CallStyle.class) & extras != null) {
Person callPerson = extras.getParcelable(EXTRA_CALL_PERSON, Person.class);
if (callPerson != null) {
- visitor.accept(callPerson.getIconUri());
+ callPerson.visitUris(visitor);
}
visitIconUri(visitor, extras.getParcelable(EXTRA_VERIFICATION_ICON, Icon.class));
}
@@ -8833,6 +8825,18 @@
}
/**
+ * See {@link Notification#visitUris(Consumer)}.
+ *
+ * @hide
+ */
+ public void visitUris(@NonNull Consumer<Uri> visitor) {
+ visitor.accept(getDataUri());
+ if (mSender != null) {
+ mSender.visitUris(visitor);
+ }
+ }
+
+ /**
* Returns a list of messages read from the given bundle list, e.g.
* {@link #EXTRA_MESSAGES} or {@link #EXTRA_HISTORIC_MESSAGES}.
*/
diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java
index 97a794d..18fc0ce 100644
--- a/core/java/android/app/Person.java
+++ b/core/java/android/app/Person.java
@@ -24,6 +24,7 @@
import android.os.Parcelable;
import java.util.Objects;
+import java.util.function.Consumer;
/**
* Provides an immutable reference to an entity that appears repeatedly on different surfaces of the
@@ -177,6 +178,19 @@
dest.writeBoolean(mIsBot);
}
+ /**
+ * Note all {@link Uri} that are referenced internally, with the expectation that Uri permission
+ * grants will need to be issued to ensure the recipient of this object is able to render its
+ * contents.
+ * See b/281044385 for more context and examples about what happens when this isn't done
+ * correctly.
+ *
+ * @hide
+ */
+ public void visitUris(@NonNull Consumer<Uri> visitor) {
+ visitor.accept(getIconUri());
+ }
+
/** Builder for the immutable {@link Person} class. */
public static class Builder {
@Nullable private CharSequence mName;
diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
index 5311b09..baf2a47 100644
--- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java
+++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java
@@ -193,8 +193,8 @@
switch (prevState) {
// TODO(lifecycler): Extend to support all possible states.
case ON_START:
- lifecycleItem = StartActivityItem.obtain(null /* activityOptions */);
- break;
+ // Fall through to return the PAUSE item to ensure the activity is properly
+ // resumed while relaunching.
case ON_PAUSE:
lifecycleItem = PauseActivityItem.obtain();
break;
diff --git a/core/java/android/content/ContentCaptureOptions.java b/core/java/android/content/ContentCaptureOptions.java
index 856bde8..36e0529 100644
--- a/core/java/android/content/ContentCaptureOptions.java
+++ b/core/java/android/content/ContentCaptureOptions.java
@@ -76,6 +76,20 @@
public final boolean disableFlushForViewTreeAppearing;
/**
+ * Is the content capture receiver enabled.
+ *
+ * @hide
+ */
+ public final boolean enableReceiver;
+
+ /**
+ * Options for the content protection flow.
+ *
+ * @hide
+ */
+ @NonNull public final ContentProtectionOptions contentProtectionOptions;
+
+ /**
* List of activities explicitly allowlisted for content capture (or {@code null} if allowlisted
* for all acitivites in the package).
*/
@@ -94,52 +108,99 @@
* for contexts belonging to the content capture service app.
*/
public ContentCaptureOptions(int loggingLevel) {
- this(/* lite= */ true, loggingLevel, /* maxBufferSize= */ 0,
- /* idleFlushingFrequencyMs= */ 0, /* textChangeFlushingFrequencyMs= */ 0,
- /* logHistorySize= */ 0, /* disableFlushForViewTreeAppearing= */ false,
+ this(
+ /* lite= */ true,
+ loggingLevel,
+ /* maxBufferSize= */ 0,
+ /* idleFlushingFrequencyMs= */ 0,
+ /* textChangeFlushingFrequencyMs= */ 0,
+ /* logHistorySize= */ 0,
+ /* disableFlushForViewTreeAppearing= */ false,
+ /* enableReceiver= */ false,
+ new ContentProtectionOptions(
+ /* enableReceiver= */ false,
+ /* bufferSize= */ 0),
/* whitelistedComponents= */ null);
}
- /**
- * Default constructor.
- */
- public ContentCaptureOptions(int loggingLevel, int maxBufferSize, int idleFlushingFrequencyMs,
- int textChangeFlushingFrequencyMs, int logHistorySize,
- @SuppressLint({"ConcreteCollection", "NullableCollection"})
- @Nullable ArraySet<ComponentName> whitelistedComponents) {
- this(/* lite= */ false, loggingLevel, maxBufferSize, idleFlushingFrequencyMs,
- textChangeFlushingFrequencyMs, logHistorySize,
+ /** Default constructor. */
+ public ContentCaptureOptions(
+ int loggingLevel,
+ int maxBufferSize,
+ int idleFlushingFrequencyMs,
+ int textChangeFlushingFrequencyMs,
+ int logHistorySize,
+ @SuppressLint({"ConcreteCollection", "NullableCollection"}) @Nullable
+ ArraySet<ComponentName> whitelistedComponents) {
+ this(
+ /* lite= */ false,
+ loggingLevel,
+ maxBufferSize,
+ idleFlushingFrequencyMs,
+ textChangeFlushingFrequencyMs,
+ logHistorySize,
ContentCaptureManager.DEFAULT_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
+ ContentCaptureManager.DEFAULT_ENABLE_CONTENT_CAPTURE_RECEIVER,
+ new ContentProtectionOptions(
+ ContentCaptureManager.DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER,
+ ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE),
whitelistedComponents);
}
/** @hide */
- public ContentCaptureOptions(int loggingLevel, int maxBufferSize, int idleFlushingFrequencyMs,
- int textChangeFlushingFrequencyMs, int logHistorySize,
+ public ContentCaptureOptions(
+ int loggingLevel,
+ int maxBufferSize,
+ int idleFlushingFrequencyMs,
+ int textChangeFlushingFrequencyMs,
+ int logHistorySize,
boolean disableFlushForViewTreeAppearing,
- @SuppressLint({"ConcreteCollection", "NullableCollection"})
- @Nullable ArraySet<ComponentName> whitelistedComponents) {
- this(/* lite= */ false, loggingLevel, maxBufferSize, idleFlushingFrequencyMs,
- textChangeFlushingFrequencyMs, logHistorySize, disableFlushForViewTreeAppearing,
+ boolean enableReceiver,
+ @NonNull ContentProtectionOptions contentProtectionOptions,
+ @SuppressLint({"ConcreteCollection", "NullableCollection"}) @Nullable
+ ArraySet<ComponentName> whitelistedComponents) {
+ this(
+ /* lite= */ false,
+ loggingLevel,
+ maxBufferSize,
+ idleFlushingFrequencyMs,
+ textChangeFlushingFrequencyMs,
+ logHistorySize,
+ disableFlushForViewTreeAppearing,
+ enableReceiver,
+ contentProtectionOptions,
whitelistedComponents);
}
/** @hide */
@VisibleForTesting
public ContentCaptureOptions(@Nullable ArraySet<ComponentName> whitelistedComponents) {
- this(ContentCaptureManager.LOGGING_LEVEL_VERBOSE,
+ this(
+ ContentCaptureManager.LOGGING_LEVEL_VERBOSE,
ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE,
ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS,
ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS,
ContentCaptureManager.DEFAULT_LOG_HISTORY_SIZE,
ContentCaptureManager.DEFAULT_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
+ ContentCaptureManager.DEFAULT_ENABLE_CONTENT_CAPTURE_RECEIVER,
+ new ContentProtectionOptions(
+ ContentCaptureManager.DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER,
+ ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE),
whitelistedComponents);
}
- private ContentCaptureOptions(boolean lite, int loggingLevel, int maxBufferSize,
- int idleFlushingFrequencyMs, int textChangeFlushingFrequencyMs, int logHistorySize,
+ private ContentCaptureOptions(
+ boolean lite,
+ int loggingLevel,
+ int maxBufferSize,
+ int idleFlushingFrequencyMs,
+ int textChangeFlushingFrequencyMs,
+ int logHistorySize,
boolean disableFlushForViewTreeAppearing,
- @Nullable ArraySet<ComponentName> whitelistedComponents) {
+ boolean enableReceiver,
+ @NonNull ContentProtectionOptions contentProtectionOptions,
+ @SuppressLint({"ConcreteCollection", "NullableCollection"}) @Nullable
+ ArraySet<ComponentName> whitelistedComponents) {
this.lite = lite;
this.loggingLevel = loggingLevel;
this.maxBufferSize = maxBufferSize;
@@ -147,6 +208,8 @@
this.textChangeFlushingFrequencyMs = textChangeFlushingFrequencyMs;
this.logHistorySize = logHistorySize;
this.disableFlushForViewTreeAppearing = disableFlushForViewTreeAppearing;
+ this.enableReceiver = enableReceiver;
+ this.contentProtectionOptions = contentProtectionOptions;
this.whitelistedComponents = whitelistedComponents;
}
@@ -191,12 +254,22 @@
return "ContentCaptureOptions [loggingLevel=" + loggingLevel + " (lite)]";
}
final StringBuilder string = new StringBuilder("ContentCaptureOptions [");
- string.append("loggingLevel=").append(loggingLevel)
- .append(", maxBufferSize=").append(maxBufferSize)
- .append(", idleFlushingFrequencyMs=").append(idleFlushingFrequencyMs)
- .append(", textChangeFlushingFrequencyMs=").append(textChangeFlushingFrequencyMs)
- .append(", logHistorySize=").append(logHistorySize)
- .append(", disableFlushForViewTreeAppearing=").append(disableFlushForViewTreeAppearing);
+ string.append("loggingLevel=")
+ .append(loggingLevel)
+ .append(", maxBufferSize=")
+ .append(maxBufferSize)
+ .append(", idleFlushingFrequencyMs=")
+ .append(idleFlushingFrequencyMs)
+ .append(", textChangeFlushingFrequencyMs=")
+ .append(textChangeFlushingFrequencyMs)
+ .append(", logHistorySize=")
+ .append(logHistorySize)
+ .append(", disableFlushForViewTreeAppearing=")
+ .append(disableFlushForViewTreeAppearing)
+ .append(", enableReceiver=")
+ .append(enableReceiver)
+ .append(", contentProtectionOptions=")
+ .append(contentProtectionOptions);
if (whitelistedComponents != null) {
string.append(", whitelisted=").append(whitelistedComponents);
}
@@ -210,11 +283,21 @@
pw.print(", lite");
return;
}
- pw.print(", bufferSize="); pw.print(maxBufferSize);
- pw.print(", idle="); pw.print(idleFlushingFrequencyMs);
- pw.print(", textIdle="); pw.print(textChangeFlushingFrequencyMs);
- pw.print(", logSize="); pw.print(logHistorySize);
- pw.print(", disableFlushForViewTreeAppearing="); pw.print(disableFlushForViewTreeAppearing);
+ pw.print(", bufferSize=");
+ pw.print(maxBufferSize);
+ pw.print(", idle=");
+ pw.print(idleFlushingFrequencyMs);
+ pw.print(", textIdle=");
+ pw.print(textChangeFlushingFrequencyMs);
+ pw.print(", logSize=");
+ pw.print(logHistorySize);
+ pw.print(", disableFlushForViewTreeAppearing=");
+ pw.print(disableFlushForViewTreeAppearing);
+ pw.print(", enableReceiver=");
+ pw.print(enableReceiver);
+ pw.print(", contentProtectionOptions=[");
+ contentProtectionOptions.dumpShort(pw);
+ pw.print("]");
if (whitelistedComponents != null) {
pw.print(", whitelisted="); pw.print(whitelistedComponents);
}
@@ -236,6 +319,8 @@
parcel.writeInt(textChangeFlushingFrequencyMs);
parcel.writeInt(logHistorySize);
parcel.writeBoolean(disableFlushForViewTreeAppearing);
+ parcel.writeBoolean(enableReceiver);
+ contentProtectionOptions.writeToParcel(parcel);
parcel.writeArraySet(whitelistedComponents);
}
@@ -254,12 +339,22 @@
final int textChangeFlushingFrequencyMs = parcel.readInt();
final int logHistorySize = parcel.readInt();
final boolean disableFlushForViewTreeAppearing = parcel.readBoolean();
+ final boolean enableReceiver = parcel.readBoolean();
+ final ContentProtectionOptions contentProtectionOptions =
+ ContentProtectionOptions.createFromParcel(parcel);
@SuppressWarnings("unchecked")
final ArraySet<ComponentName> whitelistedComponents =
(ArraySet<ComponentName>) parcel.readArraySet(null);
- return new ContentCaptureOptions(loggingLevel, maxBufferSize,
- idleFlushingFrequencyMs, textChangeFlushingFrequencyMs, logHistorySize,
- disableFlushForViewTreeAppearing, whitelistedComponents);
+ return new ContentCaptureOptions(
+ loggingLevel,
+ maxBufferSize,
+ idleFlushingFrequencyMs,
+ textChangeFlushingFrequencyMs,
+ logHistorySize,
+ disableFlushForViewTreeAppearing,
+ enableReceiver,
+ contentProtectionOptions,
+ whitelistedComponents);
}
@Override
@@ -267,4 +362,62 @@
return new ContentCaptureOptions[size];
}
};
+
+ /**
+ * Content protection options for a given package.
+ *
+ * <p>Does not implement {@code Parcelable} since it is an inner class without a matching AIDL.
+ *
+ * @hide
+ */
+ public static class ContentProtectionOptions {
+
+ /**
+ * Is the content protection receiver enabled.
+ *
+ * @hide
+ */
+ public final boolean enableReceiver;
+
+ /**
+ * Size of the in-memory ring buffer for the content protection flow.
+ *
+ * @hide
+ */
+ public final int bufferSize;
+
+ public ContentProtectionOptions(boolean enableReceiver, int bufferSize) {
+ this.enableReceiver = enableReceiver;
+ this.bufferSize = bufferSize;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder("ContentProtectionOptions [");
+ stringBuilder
+ .append("enableReceiver=")
+ .append(enableReceiver)
+ .append(", bufferSize=")
+ .append(bufferSize);
+ return stringBuilder.append(']').toString();
+ }
+
+ private void dumpShort(@NonNull PrintWriter pw) {
+ pw.print("enableReceiver=");
+ pw.print(enableReceiver);
+ pw.print(", bufferSize=");
+ pw.print(bufferSize);
+ }
+
+ private void writeToParcel(Parcel parcel) {
+ parcel.writeBoolean(enableReceiver);
+ parcel.writeInt(bufferSize);
+ }
+
+ private static ContentProtectionOptions createFromParcel(Parcel parcel) {
+ boolean enableReceiver = parcel.readBoolean();
+ int bufferSize = parcel.readInt();
+ return new ContentProtectionOptions(enableReceiver, bufferSize);
+ }
+ }
}
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 17bbe14..33960c0 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -266,7 +266,7 @@
*/
public static boolean useTouchpadTapToClick(@NonNull Context context) {
return Settings.System.getIntForUser(context.getContentResolver(),
- Settings.System.TOUCHPAD_TAP_TO_CLICK, 0, UserHandle.USER_CURRENT) == 1;
+ Settings.System.TOUCHPAD_TAP_TO_CLICK, 1, UserHandle.USER_CURRENT) == 1;
}
/**
diff --git a/core/java/android/hardware/lights/Light.java b/core/java/android/hardware/lights/Light.java
index 1df9b75..18d0b09 100644
--- a/core/java/android/hardware/lights/Light.java
+++ b/core/java/android/hardware/lights/Light.java
@@ -110,6 +110,8 @@
private final int mOrdinal;
private final int mType;
private final int mCapabilities;
+ @Nullable
+ private final int[] mPreferredBrightnessLevels;
/**
* Creates a new light with the given data.
@@ -117,7 +119,7 @@
* @hide
*/
public Light(int id, int ordinal, int type) {
- this(id, "Light", ordinal, type, 0);
+ this(id, "Light", ordinal, type, 0, null);
}
/**
@@ -126,11 +128,22 @@
* @hide
*/
public Light(int id, String name, int ordinal, int type, int capabilities) {
+ this(id, name, ordinal, type, capabilities, null);
+ }
+
+ /**
+ * Creates a new light with the given data.
+ *
+ * @hide
+ */
+ public Light(int id, String name, int ordinal, int type, int capabilities,
+ @Nullable int[] preferredBrightnessLevels) {
mId = id;
mName = name;
mOrdinal = ordinal;
mType = type;
mCapabilities = capabilities;
+ mPreferredBrightnessLevels = preferredBrightnessLevels;
}
private Light(@NonNull Parcel in) {
@@ -139,6 +152,7 @@
mOrdinal = in.readInt();
mType = in.readInt();
mCapabilities = in.readInt();
+ mPreferredBrightnessLevels = in.createIntArray();
}
/** Implement the Parcelable interface */
@@ -149,6 +163,7 @@
dest.writeInt(mOrdinal);
dest.writeInt(mType);
dest.writeInt(mCapabilities);
+ dest.writeIntArray(mPreferredBrightnessLevels);
}
/** Implement the Parcelable interface */
@@ -252,4 +267,17 @@
return (mCapabilities & LIGHT_CAPABILITY_COLOR_RGB) == LIGHT_CAPABILITY_COLOR_RGB;
}
+ /**
+ * Returns preferred brightness levels for the light which will be used when user
+ * increase/decrease brightness levels for the light (currently only used for Keyboard
+ * backlight control using backlight up/down keys).
+ *
+ * The values in the preferred brightness level array are in the range [0, 255].
+ *
+ * @hide
+ */
+ @Nullable
+ public int[] getPreferredBrightnessLevels() {
+ return mPreferredBrightnessLevels;
+ }
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d695c0c..d425bf8 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7486,6 +7486,14 @@
public static final String STYLUS_BUTTONS_ENABLED = "stylus_buttons_enabled";
/**
+ * Preferred default user profile to use with the notes task button shortcut.
+ *
+ * @hide
+ */
+ @SuppressLint("NoSettingsProvider")
+ public static final String DEFAULT_NOTE_TASK_PROFILE = "default_note_task_profile";
+
+ /**
* Host name and port for global http proxy. Uses ':' seperator for
* between host and port.
*
@@ -17979,6 +17987,15 @@
"review_permissions_notification_state";
/**
+ * Whether repair mode is active on the device.
+ * <p>
+ * Set to 1 for true and 0 for false.
+ *
+ * @hide
+ */
+ public static final String REPAIR_MODE_ACTIVE = "repair_mode_active";
+
+ /**
* Settings migrated from Wear OS settings provider.
* @hide
*/
diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java
index 470c280..a47f34f 100644
--- a/core/java/android/view/InsetsFrameProvider.java
+++ b/core/java/android/view/InsetsFrameProvider.java
@@ -164,6 +164,10 @@
return mFlags;
}
+ public boolean hasFlags(@Flags int mask) {
+ return (mFlags & mask) == mask;
+ }
+
public InsetsFrameProvider setInsetsSize(Insets insetsSize) {
mInsetsSize = insetsSize;
return this;
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index 9fc42ff..e101849 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -58,9 +58,20 @@
*/
public static final int FLAG_SUPPRESS_SCRIM = 1;
+ /**
+ * Controls whether the insets frame will be used to move {@link RoundedCorner} inward with the
+ * insets frame size when calculating the rounded corner insets to other windows.
+ *
+ * For example, task bar will draw fake rounded corners above itself, so we need to move the
+ * rounded corner up by the task bar insets size to make other windows see a rounded corner
+ * above the task bar.
+ */
+ public static final int FLAG_INSETS_ROUNDED_CORNER = 1 << 1;
+
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = "FLAG_", value = {
FLAG_SUPPRESS_SCRIM,
+ FLAG_INSETS_ROUNDED_CORNER,
})
public @interface Flags {}
@@ -78,7 +89,6 @@
private @Nullable Rect mVisibleFrame;
private boolean mVisible;
- private boolean mInsetsRoundedCornerFrame;
private final Rect mTmpFrame = new Rect();
@@ -98,7 +108,6 @@
? new Rect(other.mVisibleFrame)
: null;
mFlags = other.mFlags;
- mInsetsRoundedCornerFrame = other.mInsetsRoundedCornerFrame;
}
public void set(InsetsSource other) {
@@ -108,7 +117,6 @@
? new Rect(other.mVisibleFrame)
: null;
mFlags = other.mFlags;
- mInsetsRoundedCornerFrame = other.mInsetsRoundedCornerFrame;
}
public InsetsSource setFrame(int left, int top, int right, int bottom) {
@@ -136,6 +144,11 @@
return this;
}
+ public InsetsSource setFlags(@Flags int flags, @Flags int mask) {
+ mFlags = (mFlags & ~mask) | (flags & mask);
+ return this;
+ }
+
public int getId() {
return mId;
}
@@ -160,20 +173,15 @@
return mFlags;
}
+ public boolean hasFlags(int flags) {
+ return (mFlags & flags) == flags;
+ }
+
boolean isUserControllable() {
// If mVisibleFrame is null, it will be the same area as mFrame.
return mVisibleFrame == null || !mVisibleFrame.isEmpty();
}
- public boolean insetsRoundedCornerFrame() {
- return mInsetsRoundedCornerFrame;
- }
-
- public InsetsSource setInsetsRoundedCornerFrame(boolean insetsRoundedCornerFrame) {
- mInsetsRoundedCornerFrame = insetsRoundedCornerFrame;
- return this;
- }
-
/**
* Calculates the insets this source will cause to a client window.
*
@@ -317,6 +325,9 @@
if ((flags & FLAG_SUPPRESS_SCRIM) != 0) {
joiner.add("SUPPRESS_SCRIM");
}
+ if ((flags & FLAG_INSETS_ROUNDED_CORNER) != 0) {
+ joiner.add("INSETS_ROUNDED_CORNER");
+ }
return joiner.toString();
}
@@ -347,7 +358,6 @@
}
pw.print(" visible="); pw.print(mVisible);
pw.print(" flags="); pw.print(flagsToString(mFlags));
- pw.print(" insetsRoundedCornerFrame="); pw.print(mInsetsRoundedCornerFrame);
pw.println();
}
@@ -372,14 +382,12 @@
if (mFlags != that.mFlags) return false;
if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true;
if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false;
- if (mInsetsRoundedCornerFrame != that.mInsetsRoundedCornerFrame) return false;
return mFrame.equals(that.mFrame);
}
@Override
public int hashCode() {
- return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags,
- mInsetsRoundedCornerFrame);
+ return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags);
}
public InsetsSource(Parcel in) {
@@ -393,7 +401,6 @@
}
mVisible = in.readBoolean();
mFlags = in.readInt();
- mInsetsRoundedCornerFrame = in.readBoolean();
}
@Override
@@ -414,7 +421,6 @@
}
dest.writeBoolean(mVisible);
dest.writeInt(mFlags);
- dest.writeBoolean(mInsetsRoundedCornerFrame);
}
@Override
@@ -424,7 +430,6 @@
+ " mFrame=" + mFrame.toShortString()
+ " mVisible=" + mVisible
+ " mFlags=[" + flagsToString(mFlags) + "]"
- + (mInsetsRoundedCornerFrame ? " insetsRoundedCornerFrame" : "")
+ "}";
}
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 5b974cd..61a7277 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -16,6 +16,7 @@
package android.view;
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
import static android.view.InsetsStateProto.DISPLAY_CUTOUT;
import static android.view.InsetsStateProto.DISPLAY_FRAME;
import static android.view.InsetsStateProto.SOURCES;
@@ -219,7 +220,7 @@
final Rect roundedCornerFrame = new Rect(mRoundedCornerFrame);
for (int i = mSources.size() - 1; i >= 0; i--) {
final InsetsSource source = mSources.valueAt(i);
- if (source.insetsRoundedCornerFrame()) {
+ if (source.hasFlags(FLAG_INSETS_ROUNDED_CORNER)) {
final Insets insets = source.calculateInsets(roundedCornerFrame, false);
roundedCornerFrame.inset(insets);
}
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 1af8ca2..c703af5 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -1285,6 +1285,8 @@
* </ul>
* These values are relative to the state from the last event, not accumulated, so developers
* should make sure to process this axis value for all batched historical events.
+ * <p>
+ * This axis is only set on the first pointer in a motion event.
*/
public static final int AXIS_GESTURE_X_OFFSET = 48;
@@ -1304,6 +1306,8 @@
* </ul>
* These values are relative to the state from the last event, not accumulated, so developers
* should make sure to process this axis value for all batched historical events.
+ * <p>
+ * This axis is only set on the first pointer in a motion event.
*/
public static final int AXIS_GESTURE_SCROLL_X_DISTANCE = 50;
@@ -1324,14 +1328,29 @@
* </ul>
* These values are relative to the state from the last event, not accumulated, so developers
* should make sure to process this axis value for all batched historical events.
+ * <p>
+ * This axis is only set on the first pointer in a motion event.
*/
public static final int AXIS_GESTURE_PINCH_SCALE_FACTOR = 52;
+ /**
+ * Axis constant: the number of fingers being used in a multi-finger swipe gesture.
+ * <p>
+ * <ul>
+ * <li>For a touch pad, reports the number of fingers being used in a multi-finger swipe gesture
+ * (with CLASSIFICATION_MULTI_FINGER_SWIPE).
+ * </ul>
+ * <p>
+ * Since CLASSIFICATION_MULTI_FINGER_SWIPE is a hidden API, so is this axis. It is only set on
+ * the first pointer in a motion event.
+ * @hide
+ */
+ public static final int AXIS_GESTURE_SWIPE_FINGER_COUNT = 53;
+
// NOTE: If you add a new axis here you must also add it to:
// frameworks/native/include/android/input.h
// frameworks/native/libs/input/InputEventLabels.cpp
- // platform/cts/tests/tests/view/src/android/view/cts/MotionEventTest.java
- // (testAxisFromToString)
+ // cts/tests/tests/view/src/android/view/cts/MotionEventTest.java (testAxisFromToString)
// Symbolic names of all axes.
private static final SparseArray<String> AXIS_SYMBOLIC_NAMES = new SparseArray<String>();
@@ -1387,6 +1406,7 @@
names.append(AXIS_GESTURE_SCROLL_X_DISTANCE, "AXIS_GESTURE_SCROLL_X_DISTANCE");
names.append(AXIS_GESTURE_SCROLL_Y_DISTANCE, "AXIS_GESTURE_SCROLL_Y_DISTANCE");
names.append(AXIS_GESTURE_PINCH_SCALE_FACTOR, "AXIS_GESTURE_PINCH_SCALE_FACTOR");
+ names.append(AXIS_GESTURE_SWIPE_FINGER_COUNT, "AXIS_GESTURE_SWIPE_FINGER_COUNT");
}
/**
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index d457847..01a99b9 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -2043,7 +2043,10 @@
final float x = event.getXDispatchLocation(pointerIndex);
final float y = event.getYDispatchLocation(pointerIndex);
if (isOnScrollbarThumb(x, y) || isDraggingScrollBar()) {
- return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_ARROW);
+ // Return null here so that it fallbacks to the default PointerIcon for the source
+ // device. For mouse, the default PointerIcon is PointerIcon.TYPE_ARROW.
+ // For stylus, the default PointerIcon is PointerIcon.TYPE_NULL.
+ return null;
}
// Check what the child under the pointer says about the pointer.
final int childrenCount = mChildrenCount;
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 21fe87f..7596459 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -1482,6 +1482,11 @@
}
/** @hide */
+ public boolean shouldCloseOnTouchOutside() {
+ return mCloseOnTouchOutside;
+ }
+
+ /** @hide */
@SuppressWarnings("HiddenAbstractMethod")
@UnsupportedAppUsage
public abstract void alwaysReadCloseOnTouchAttr();
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index e40f8e7..d40c032 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -4252,17 +4252,6 @@
public InsetsFrameProvider[] providedInsets;
/**
- * If specified, the frame that used to calculate relative {@link RoundedCorner} will be
- * the window frame of this window minus the insets that this window provides.
- *
- * Task bar will draw fake rounded corners above itself, so we need this insets to calculate
- * correct rounded corners for this window.
- *
- * @hide
- */
- public boolean insetsRoundedCornerFrame = false;
-
- /**
* {@link LayoutParams} to be applied to the window when layout with a assigned rotation.
* This will make layout during rotation change smoothly.
*
@@ -4718,7 +4707,6 @@
out.writeBoolean(mFitInsetsIgnoringVisibility);
out.writeBoolean(preferMinimalPostProcessing);
out.writeInt(mBlurBehindRadius);
- out.writeBoolean(insetsRoundedCornerFrame);
out.writeBoolean(mWallpaperTouchEventsEnabled);
out.writeTypedArray(providedInsets, 0 /* parcelableFlags */);
checkNonRecursiveParams();
@@ -4790,7 +4778,6 @@
mFitInsetsIgnoringVisibility = in.readBoolean();
preferMinimalPostProcessing = in.readBoolean();
mBlurBehindRadius = in.readInt();
- insetsRoundedCornerFrame = in.readBoolean();
mWallpaperTouchEventsEnabled = in.readBoolean();
providedInsets = in.createTypedArray(InsetsFrameProvider.CREATOR);
paramsForRotation = in.createTypedArray(LayoutParams.CREATOR);
@@ -5098,11 +5085,6 @@
changes |= LAYOUT_CHANGED;
}
- if (insetsRoundedCornerFrame != o.insetsRoundedCornerFrame) {
- insetsRoundedCornerFrame = o.insetsRoundedCornerFrame;
- changes |= LAYOUT_CHANGED;
- }
-
if (paramsForRotation != o.paramsForRotation) {
if ((changes & LAYOUT_CHANGED) == 0) {
if (paramsForRotation != null && o.paramsForRotation != null
@@ -5340,10 +5322,6 @@
sb.append(prefix).append(" ").append(providedInsets[i]);
}
}
- if (insetsRoundedCornerFrame) {
- sb.append(" insetsRoundedCornerFrame=");
- sb.append(insetsRoundedCornerFrame);
- }
if (paramsForRotation != null && paramsForRotation.length != 0) {
sb.append(System.lineSeparator());
sb.append(prefix).append(" paramsForRotation:");
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 668351b..c37e311 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -352,6 +352,30 @@
public static final String DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING =
"disable_flush_for_view_tree_appearing";
+ /**
+ * Enables the content protection receiver.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER =
+ "enable_content_protection_receiver";
+
+ /**
+ * Sets the size of the app blocklist for the content protection flow.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE =
+ "content_protection_apps_blocklist_size";
+
+ /**
+ * Sets the size of the in-memory ring buffer for the content protection flow.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE =
+ "content_protection_buffer_size";
+
/** @hide */
@TestApi
public static final int LOGGING_LEVEL_OFF = 0;
@@ -384,6 +408,14 @@
public static final int DEFAULT_LOG_HISTORY_SIZE = 10;
/** @hide */
public static final boolean DEFAULT_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING = false;
+ /** @hide */
+ public static final boolean DEFAULT_ENABLE_CONTENT_CAPTURE_RECEIVER = true;
+ /** @hide */
+ public static final boolean DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER = false;
+ /** @hide */
+ public static final int DEFAULT_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE = 1000;
+ /** @hide */
+ public static final int DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE = 150;
private final Object mLock = new Object();
@@ -425,11 +457,11 @@
/** @hide */
static class StrippedContext {
- final String mPackageName;
- final String mContext;
+ @NonNull final String mPackageName;
+ @NonNull final String mContext;
final @UserIdInt int mUserId;
- private StrippedContext(Context context) {
+ private StrippedContext(@NonNull Context context) {
mPackageName = context.getPackageName();
mContext = context.toString();
mUserId = context.getUserId();
@@ -440,6 +472,7 @@
return mContext;
}
+ @NonNull
public String getPackageName() {
return mPackageName;
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index 62044aa..b229106 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -181,6 +181,8 @@
public static final int FLUSH_REASON_VIEW_TREE_APPEARING = 9;
/** @hide */
public static final int FLUSH_REASON_VIEW_TREE_APPEARED = 10;
+ /** @hide */
+ public static final int FLUSH_REASON_LOGIN_DETECTED = 11;
/**
* After {@link UPSIDE_DOWN_CAKE}, {@link #notifyViewsDisappeared(AutofillId, long[])} wraps
@@ -191,20 +193,23 @@
static final long NOTIFY_NODES_DISAPPEAR_NOW_SENDS_TREE_EVENTS = 258825825L;
/** @hide */
- @IntDef(prefix = { "FLUSH_REASON_" }, value = {
- FLUSH_REASON_FULL,
- FLUSH_REASON_VIEW_ROOT_ENTERED,
- FLUSH_REASON_SESSION_STARTED,
- FLUSH_REASON_SESSION_FINISHED,
- FLUSH_REASON_IDLE_TIMEOUT,
- FLUSH_REASON_TEXT_CHANGE_TIMEOUT,
- FLUSH_REASON_SESSION_CONNECTED,
- FLUSH_REASON_FORCE_FLUSH,
- FLUSH_REASON_VIEW_TREE_APPEARING,
- FLUSH_REASON_VIEW_TREE_APPEARED
- })
+ @IntDef(
+ prefix = {"FLUSH_REASON_"},
+ value = {
+ FLUSH_REASON_FULL,
+ FLUSH_REASON_VIEW_ROOT_ENTERED,
+ FLUSH_REASON_SESSION_STARTED,
+ FLUSH_REASON_SESSION_FINISHED,
+ FLUSH_REASON_IDLE_TIMEOUT,
+ FLUSH_REASON_TEXT_CHANGE_TIMEOUT,
+ FLUSH_REASON_SESSION_CONNECTED,
+ FLUSH_REASON_FORCE_FLUSH,
+ FLUSH_REASON_VIEW_TREE_APPEARING,
+ FLUSH_REASON_VIEW_TREE_APPEARED,
+ FLUSH_REASON_LOGIN_DETECTED
+ })
@Retention(RetentionPolicy.SOURCE)
- public @interface FlushReason{}
+ public @interface FlushReason {}
private final Object mLock = new Object();
@@ -685,8 +690,10 @@
return "VIEW_TREE_APPEARING";
case FLUSH_REASON_VIEW_TREE_APPEARED:
return "VIEW_TREE_APPEARED";
+ case FLUSH_REASON_LOGIN_DETECTED:
+ return "LOGIN_DETECTED";
default:
- return "UNKOWN-" + reason;
+ return "UNKNOWN-" + reason;
}
}
diff --git a/core/java/android/view/contentcapture/ViewNode.java b/core/java/android/view/contentcapture/ViewNode.java
index 044a31f..f218995 100644
--- a/core/java/android/view/contentcapture/ViewNode.java
+++ b/core/java/android/view/contentcapture/ViewNode.java
@@ -480,6 +480,11 @@
return mLocaleList;
}
+ /** @hide */
+ public void setTextIdEntry(@NonNull String textIdEntry) {
+ mTextIdEntry = textIdEntry;
+ }
+
private void writeSelfToParcel(@NonNull Parcel parcel, int parcelFlags) {
long nodeFlags = mFlags;
diff --git a/core/java/android/view/contentprotection/ContentProtectionUtils.java b/core/java/android/view/contentprotection/ContentProtectionUtils.java
new file mode 100644
index 0000000..9abf6f1
--- /dev/null
+++ b/core/java/android/view/contentprotection/ContentProtectionUtils.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.contentprotection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.ViewNode;
+
+/**
+ * Utilities for reading data from {@link ContentCaptureEvent} and {@link ViewNode}.
+ *
+ * @hide
+ */
+public final class ContentProtectionUtils {
+
+ /** Returns the text extracted directly from the {@link ContentCaptureEvent}, if set. */
+ @Nullable
+ public static String getEventText(@NonNull ContentCaptureEvent event) {
+ CharSequence text = event.getText();
+ if (text == null) {
+ return null;
+ }
+ return text.toString();
+ }
+
+ /** Returns the text extracted from the event's {@link ViewNode}, if set. */
+ @Nullable
+ public static String getViewNodeText(@NonNull ContentCaptureEvent event) {
+ ViewNode viewNode = event.getViewNode();
+ if (viewNode == null) {
+ return null;
+ }
+ return getViewNodeText(viewNode);
+ }
+
+ /** Returns the text extracted directly from the {@link ViewNode}, if set. */
+ @Nullable
+ public static String getViewNodeText(@NonNull ViewNode viewNode) {
+ CharSequence text = viewNode.getText();
+ if (text == null) {
+ return null;
+ }
+ return text.toString();
+ }
+}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 41ef44e..40b060a 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -4361,15 +4361,14 @@
* @param icProto {@link InputConnection} call data in proto format.
* @hide
*/
- @GuardedBy("mH")
public void dumpDebug(ProtoOutputStream proto, @Nullable byte[] icProto) {
- if (!isImeSessionAvailableLocked()) {
- return;
- }
-
- proto.write(DISPLAY_ID, mDisplayId);
- final long token = proto.start(INPUT_METHOD_MANAGER);
synchronized (mH) {
+ if (!isImeSessionAvailableLocked()) {
+ return;
+ }
+
+ proto.write(DISPLAY_ID, mDisplayId);
+ final long token = proto.start(INPUT_METHOD_MANAGER);
proto.write(CUR_ID, mCurBindState.mImeId);
proto.write(FULLSCREEN_MODE, mFullscreenMode);
proto.write(ACTIVE, mActive);
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index adeb889..6ad1960 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -4703,7 +4703,7 @@
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
- if (mFastScroll != null) {
+ if (mFastScroll != null && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
PointerIcon pointerIcon = mFastScroll.onResolvePointerIcon(event, pointerIndex);
if (pointerIcon != null) {
return pointerIcon;
diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java
index 634cbe3..405099d 100644
--- a/core/java/android/widget/Button.java
+++ b/core/java/android/widget/Button.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.util.AttributeSet;
+import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.PointerIcon;
@@ -173,7 +174,8 @@
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
- if (getPointerIcon() == null && isClickable() && isEnabled()) {
+ if (getPointerIcon() == null && isClickable() && isEnabled()
+ && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
}
return super.onResolvePointerIcon(event, pointerIndex);
diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java
index e1b0c91..b6c5396c 100644
--- a/core/java/android/widget/ImageButton.java
+++ b/core/java/android/widget/ImageButton.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.util.AttributeSet;
+import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.widget.RemoteViews.RemoteView;
@@ -99,7 +100,8 @@
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
- if (getPointerIcon() == null && isClickable() && isEnabled()) {
+ if (getPointerIcon() == null && isClickable() && isEnabled()
+ && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
}
return super.onResolvePointerIcon(event, pointerIndex);
diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java
index f3600b0..edf0f48 100644
--- a/core/java/android/widget/RadialTimePickerView.java
+++ b/core/java/android/widget/RadialTimePickerView.java
@@ -38,6 +38,7 @@
import android.util.StateSet;
import android.util.TypedValue;
import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
@@ -1060,9 +1061,11 @@
if (!isEnabled()) {
return null;
}
- final int degrees = getDegreesFromXY(event.getX(), event.getY(), false);
- if (degrees != -1) {
- return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+ if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ final int degrees = getDegreesFromXY(event.getX(), event.getY(), false);
+ if (degrees != -1) {
+ return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+ }
}
return super.onResolvePointerIcon(event, pointerIndex);
}
diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java
index 6c53a44..1317b51 100644
--- a/core/java/android/widget/SimpleMonthView.java
+++ b/core/java/android/widget/SimpleMonthView.java
@@ -39,6 +39,7 @@
import android.util.IntArray;
import android.util.MathUtils;
import android.util.StateSet;
+import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
@@ -1041,12 +1042,15 @@
if (!isEnabled()) {
return null;
}
- // Add 0.5f to event coordinates to match the logic in onTouchEvent.
- final int x = (int) (event.getX() + 0.5f);
- final int y = (int) (event.getY() + 0.5f);
- final int dayUnderPointer = getDayAtLocation(x, y);
- if (dayUnderPointer >= 0) {
- return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+
+ if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ // Add 0.5f to event coordinates to match the logic in onTouchEvent.
+ final int x = (int) (event.getX() + 0.5f);
+ final int y = (int) (event.getY() + 0.5f);
+ final int dayUnderPointer = getDayAtLocation(x, y);
+ if (dayUnderPointer >= 0) {
+ return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+ }
}
return super.onResolvePointerIcon(event, pointerIndex);
}
diff --git a/core/java/android/widget/Spinner.java b/core/java/android/widget/Spinner.java
index ad431ef..ecc41a5 100644
--- a/core/java/android/widget/Spinner.java
+++ b/core/java/android/widget/Spinner.java
@@ -38,6 +38,7 @@
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
+import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
@@ -935,7 +936,8 @@
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
- if (getPointerIcon() == null && isClickable() && isEnabled()) {
+ if (getPointerIcon() == null && isClickable() && isEnabled()
+ && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
}
return super.onResolvePointerIcon(event, pointerIndex);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index db7d484..7e1e52d 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9223,18 +9223,20 @@
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
- if (mSpannable != null && mLinksClickable) {
- final float x = event.getX(pointerIndex);
- final float y = event.getY(pointerIndex);
- final int offset = getOffsetForPosition(x, y);
- final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset,
- ClickableSpan.class);
- if (clickables.length > 0) {
- return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND);
+ if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+ if (mSpannable != null && mLinksClickable) {
+ final float x = event.getX(pointerIndex);
+ final float y = event.getY(pointerIndex);
+ final int offset = getOffsetForPosition(x, y);
+ final ClickableSpan[] clickables = mSpannable.getSpans(offset, offset,
+ ClickableSpan.class);
+ if (clickables.length > 0) {
+ return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND);
+ }
}
- }
- if (isTextSelectable() || isTextEditable()) {
- return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT);
+ if (isTextSelectable() || isTextEditable()) {
+ return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT);
+ }
}
return super.onResolvePointerIcon(event, pointerIndex);
}
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 8135f9c..4cb592e 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -571,6 +571,11 @@
*/
public static final String COMBINED_BROADCAST_ENABLED = "combined_broadcast_enabled";
+ /**
+ * (boolean) Whether to allow cursor hover states for certain elements.
+ */
+ public static final String CURSOR_HOVER_STATES_ENABLED = "cursor_hover_states_enabled";
+
private SystemUiDeviceConfigFlags() {
}
}
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index e530aec..869b696 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -28,6 +28,7 @@
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__IME_INSETS_ANIMATION;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_ALL_APPS_SCROLL;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_HOME;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_PIP;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_ICON;
import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_RECENTS;
@@ -258,8 +259,16 @@
public static final int CUJ_IME_INSETS_ANIMATION = 69;
public static final int CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION = 70;
public static final int CUJ_LAUNCHER_OPEN_SEARCH_RESULT = 71;
+ // 72 - 77 are reserved for b/281564325.
- private static final int LAST_CUJ = CUJ_LAUNCHER_OPEN_SEARCH_RESULT;
+ /**
+ * In some cases when we do not have any end-target, we play a simple slide-down animation.
+ * eg: Open an app from Overview/Task switcher such that there is no home-screen icon.
+ * eg: Exit the app using back gesture.
+ */
+ public static final int CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK = 78;
+
+ private static final int LAST_CUJ = CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK;
private static final int NO_STATSD_LOGGING = -1;
// Used to convert CujType to InteractionType enum value for statsd logging.
@@ -340,6 +349,14 @@
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_IME_INSETS_ANIMATION] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__IME_INSETS_ANIMATION;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_CLOCK_MOVE_ANIMATION;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_OPEN_SEARCH_RESULT] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_OPEN_SEARCH_RESULT;
+ // 72 - 77 are reserved for b/281564325.
+ CUJ_TO_STATSD_INTERACTION_TYPE[72] = NO_STATSD_LOGGING;
+ CUJ_TO_STATSD_INTERACTION_TYPE[73] = NO_STATSD_LOGGING;
+ CUJ_TO_STATSD_INTERACTION_TYPE[74] = NO_STATSD_LOGGING;
+ CUJ_TO_STATSD_INTERACTION_TYPE[75] = NO_STATSD_LOGGING;
+ CUJ_TO_STATSD_INTERACTION_TYPE[76] = NO_STATSD_LOGGING;
+ CUJ_TO_STATSD_INTERACTION_TYPE[77] = NO_STATSD_LOGGING;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK] = UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK;
}
private static class InstanceHolder {
@@ -439,6 +456,7 @@
CUJ_IME_INSETS_ANIMATION,
CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION,
CUJ_LAUNCHER_OPEN_SEARCH_RESULT,
+ CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {
@@ -1050,6 +1068,8 @@
return "LOCKSCREEN_CLOCK_MOVE_ANIMATION";
case CUJ_LAUNCHER_OPEN_SEARCH_RESULT:
return "LAUNCHER_OPEN_SEARCH_RESULT";
+ case CUJ_LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK:
+ return "LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/statusbar/IAppClipsService.aidl b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
index 013d0d3..d6ab8bc 100644
--- a/core/java/com/android/internal/statusbar/IAppClipsService.aidl
+++ b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
@@ -23,4 +23,6 @@
*/
interface IAppClipsService {
boolean canLaunchCaptureContentActivityForNote(in int taskId);
-}
\ No newline at end of file
+
+ int canLaunchCaptureContentActivityForNoteInternal(in int taskId);
+}
diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index f55d15d..fb4b026 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -163,6 +163,8 @@
@UnsupportedAppUsage
private boolean mPrintCoords = true;
+ private float mDensity;
+
public PointerLocationView(Context c) {
super(c);
setFocusableInTouchMode(true);
@@ -357,19 +359,20 @@
// Draw current touch ellipse.
mPaint.setARGB(255, pressureLevel, 255 - pressureLevel, 128);
- drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor,
- ps.mCoords.touchMinor, ps.mCoords.orientation, mPaint);
+ drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.touchMajor * mDensity,
+ ps.mCoords.touchMinor * mDensity, ps.mCoords.orientation, mPaint);
// Draw current tool ellipse.
mPaint.setARGB(255, pressureLevel, 128, 255 - pressureLevel);
- drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor,
- ps.mCoords.toolMinor, ps.mCoords.orientation, mPaint);
+ drawOval(canvas, ps.mCoords.x, ps.mCoords.y, ps.mCoords.toolMajor * mDensity,
+ ps.mCoords.toolMinor * mDensity, ps.mCoords.orientation, mPaint);
// Draw the orientation arrow.
float arrowSize = ps.mCoords.toolMajor * 0.7f;
if (arrowSize < 20) {
arrowSize = 20;
}
+ arrowSize *= mDensity;
mPaint.setARGB(255, pressureLevel, 255, 0);
float orientationVectorX = (float) (Math.sin(ps.mCoords.orientation)
* arrowSize);
@@ -398,7 +401,7 @@
canvas.drawCircle(
ps.mCoords.x + orientationVectorX * tiltScale,
ps.mCoords.y + orientationVectorY * tiltScale,
- 3.0f, mPaint);
+ 3.0f * mDensity, mPaint);
// Draw the current bounding box
if (ps.mHasBoundingBox) {
@@ -1003,10 +1006,10 @@
// Compute size by display density.
private void configureDensityDependentFactors() {
- final float density = getResources().getDisplayMetrics().density;
- mTextPaint.setTextSize(10 * density);
- mPaint.setStrokeWidth(1 * density);
- mCurrentPointPaint.setStrokeWidth(1 * density);
- mPathPaint.setStrokeWidth(1 * density);
+ mDensity = getResources().getDisplayMetrics().density;
+ mTextPaint.setTextSize(10 * mDensity);
+ mPaint.setStrokeWidth(1 * mDensity);
+ mCurrentPointPaint.setStrokeWidth(1 * mDensity);
+ mPathPaint.setStrokeWidth(1 * mDensity);
}
}
diff --git a/core/proto/android/input/keyboard_configured.proto b/core/proto/android/input/keyboard_configured.proto
new file mode 100644
index 0000000..1699008
--- /dev/null
+++ b/core/proto/android/input/keyboard_configured.proto
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package com.android.internal.os;
+
+option java_outer_classname = "KeyboardConfiguredProto";
+
+/**
+ * RepeatedKeyboardLayout proto from input_extension_atoms.proto,
+ * duplicated here so that it's accessible in the build.
+ * Must be kept in sync with the version in input_extension_atoms.proto.
+ */
+
+// Message containing the repeated field for KeyboardLayoutConfig
+message RepeatedKeyboardLayoutConfig {
+ repeated KeyboardLayoutConfig keyboard_layout_config = 1;
+}
+
+// Keyboard layout configured when the device is connected
+// used in KeyboardConfigured atom
+message KeyboardLayoutConfig {
+ // Keyboard configuration details
+ // Layout type mappings found at:
+ // frameworks/base/core/res/res/values/attrs.xml
+ optional int32 keyboard_layout_type = 1;
+ // PK language language tag (e.g. en-US, ru-Cyrl, etc). This will follow
+ // BCP-47 language tag standards.
+ optional string keyboard_language_tag = 2;
+ // Selected keyboard layout name (e.g. English(US), English(Dvorak), etc.)
+ optional string keyboard_layout_name = 3;
+ // Criteria for layout selection (such as user, device, virtual keyboard based)
+ // IntDef annotation at:
+ // services/core/java/com/android/server/input/KeyboardMetricsCollector.java
+ optional int32 layout_selection_criteria = 4;
+}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 4ef9efd..6353cf2 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4429,6 +4429,14 @@
-->
<string name="config_defaultContentCaptureService" translatable="false"></string>
+ <!-- The package name for the system's content protection service.
+ This service must be trusted, as it can be activated without explicit consent of the user.
+ If no service with the specified name exists on the device, content protection will be
+ disabled.
+ Example: "com.android.contentprotection/.ContentProtectionService"
+ -->
+ <string name="config_defaultContentProtectionService" translatable="false"></string>
+
<!-- The package name for the system's augmented autofill service.
This service must be trusted, as it can be activated without explicit consent of the user.
If no service with the specified name exists on the device, augmented autofill wil be
@@ -6198,7 +6206,7 @@
<!-- Flag indicating whether the show Stylus pointer icon.
If set, a pointer icon will be shown over the location of a stylus pointer.-->
- <bool name="config_enableStylusPointerIcon">false</bool>
+ <bool name="config_enableStylusPointerIcon">true</bool>
<!-- Determines whether SafetyCenter feature is enabled. -->
<bool name="config_enableSafetyCenter">true</bool>
@@ -6459,4 +6467,9 @@
<!-- Whether the AOSP support for app cloning building blocks is to be enabled for the
device. -->
<bool name="config_enableAppCloningBuildingBlocks">true</bool>
+
+ <!-- Enables or disables support for repair mode. The feature creates a secure
+ environment to protect the user's privacy when the device is being repaired.
+ Off by default, since OEMs may have had a similar feature on their devices. -->
+ <bool name="config_repairModeSupported">false</bool>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8e34412..387955f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3764,6 +3764,7 @@
<java-symbol type="string" name="config_defaultTextClassifierPackage" />
<java-symbol type="string" name="config_defaultWellbeingPackage" />
<java-symbol type="string" name="config_defaultContentCaptureService" />
+ <java-symbol type="string" name="config_defaultContentProtectionService" />
<java-symbol type="string" name="config_defaultAugmentedAutofillService" />
<java-symbol type="string" name="config_defaultTranslationService" />
<java-symbol type="string" name="config_defaultAppPredictionService" />
@@ -4914,6 +4915,8 @@
<java-symbol type="bool" name="config_safetyProtectionEnabled" />
+ <java-symbol type="bool" name="config_repairModeSupported" />
+
<java-symbol type="string" name="config_devicePolicyManagementUpdater" />
<java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" />
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index c1deba3..129de64 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1716,6 +1716,17 @@
<meta-data android:name="android.view.im"
android:resource="@xml/ime_meta_handwriting"/>
</service>
+
+ <activity android:name="android.widget.PointerIconTestActivity"
+ android:label="PointerIconTestActivity"
+ android:screenOrientation="portrait"
+ android:exported="true"
+ android:theme="@android:style/Theme.Material.Light">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+ </intent-filter>
+ </activity>
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/core/tests/coretests/res/layout/pointer_icon_test.xml b/core/tests/coretests/res/layout/pointer_icon_test.xml
new file mode 100644
index 0000000..a2a6447
--- /dev/null
+++ b/core/tests/coretests/res/layout/pointer_icon_test.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TextView
+ android:id="@+id/textview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Test"/>
+
+ <EditText
+ android:id="@+id/edittext"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Test"/>
+
+ <Button
+ android:id="@+id/button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Test"/>
+
+ <ImageButton
+ android:id="@+id/imagebutton"
+ android:layout_width="50dp"
+ android:layout_height="50dp"/>
+
+ <Spinner
+ android:id="@+id/spinner"
+ android:layout_width="50dp"
+ android:layout_height="50dp"/>
+
+ <RadialTimePickerView
+ android:id="@+id/timepicker"
+ android:layout_width="200dp"
+ android:layout_height="200dp"/>
+
+ <CalendarView
+ android:id="@+id/calendar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java b/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java
index c6f4fa2..f8348d2 100644
--- a/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java
+++ b/core/tests/coretests/src/android/content/ContentCaptureOptionsTest.java
@@ -22,6 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.Parcel;
import android.util.ArraySet;
import android.view.contentcapture.ContentCaptureManager.ContentCaptureClient;
@@ -40,16 +41,29 @@
@RunWith(MockitoJUnitRunner.class)
public class ContentCaptureOptionsTest {
- private final ComponentName mContextComponent = new ComponentName("marco", "polo");
- private final ComponentName mComp1 = new ComponentName("comp", "one");
- private final ComponentName mComp2 = new ComponentName("two", "comp");
+ private static final ComponentName CONTEXT_COMPONENT = new ComponentName("marco", "polo");
+ private static final ComponentName COMPONENT1 = new ComponentName("comp", "one");
+ private static final ComponentName COMPONENT2 = new ComponentName("two", "comp");
+ private static final ContentCaptureOptions CONTENT_CAPTURE_OPTIONS =
+ new ContentCaptureOptions(
+ /* loggingLevel= */ 1000,
+ /* maxBufferSize= */ 1001,
+ /* idleFlushingFrequencyMs= */ 1002,
+ /* textChangeFlushingFrequencyMs= */ 1003,
+ /* logHistorySize= */ 1004,
+ /* disableFlushForViewTreeAppearing= */ true,
+ /* enableReceiver= */ false,
+ new ContentCaptureOptions.ContentProtectionOptions(
+ /* enableReceiver= */ true,
+ /* bufferSize= */ 2001),
+ /* whitelistedComponents= */ toSet(COMPONENT1, COMPONENT2));
@Mock private Context mContext;
@Mock private ContentCaptureClient mClient;
@Before
public void setExpectation() {
- when(mClient.contentCaptureClientGetComponentName()).thenReturn(mContextComponent);
+ when(mClient.contentCaptureClientGetComponentName()).thenReturn(CONTEXT_COMPONENT);
when(mContext.getContentCaptureClient()).thenReturn(mClient);
}
@@ -67,26 +81,27 @@
@Test
public void testIsWhitelisted_notWhitelisted() {
- ContentCaptureOptions options = new ContentCaptureOptions(toSet(mComp1, mComp2));
+ ContentCaptureOptions options = new ContentCaptureOptions(toSet(COMPONENT1, COMPONENT2));
assertThat(options.isWhitelisted(mContext)).isFalse();
}
@Test
public void testIsWhitelisted_whitelisted() {
- ContentCaptureOptions options = new ContentCaptureOptions(toSet(mComp1, mContextComponent));
+ ContentCaptureOptions options =
+ new ContentCaptureOptions(toSet(COMPONENT1, CONTEXT_COMPONENT));
assertThat(options.isWhitelisted(mContext)).isTrue();
}
@Test
public void testIsWhitelisted_invalidContext() {
- ContentCaptureOptions options = new ContentCaptureOptions(toSet(mContextComponent));
+ ContentCaptureOptions options = new ContentCaptureOptions(toSet(CONTEXT_COMPONENT));
Context invalidContext = mock(Context.class); // has no client
assertThat(options.isWhitelisted(invalidContext)).isFalse();
}
@Test
public void testIsWhitelisted_clientWithNullComponentName() {
- ContentCaptureOptions options = new ContentCaptureOptions(toSet(mContextComponent));
+ ContentCaptureOptions options = new ContentCaptureOptions(toSet(CONTEXT_COMPONENT));
ContentCaptureClient client = mock(ContentCaptureClient.class);
Context context = mock(Context.class);
when(context.getContentCaptureClient()).thenReturn(client);
@@ -94,8 +109,69 @@
assertThat(options.isWhitelisted(context)).isFalse();
}
+ @Test
+ public void testToString() {
+ String actual = CONTENT_CAPTURE_OPTIONS.toString();
+
+ String expected =
+ new StringBuilder("ContentCaptureOptions [")
+ .append("loggingLevel=")
+ .append(CONTENT_CAPTURE_OPTIONS.loggingLevel)
+ .append(", maxBufferSize=")
+ .append(CONTENT_CAPTURE_OPTIONS.maxBufferSize)
+ .append(", idleFlushingFrequencyMs=")
+ .append(CONTENT_CAPTURE_OPTIONS.idleFlushingFrequencyMs)
+ .append(", textChangeFlushingFrequencyMs=")
+ .append(CONTENT_CAPTURE_OPTIONS.textChangeFlushingFrequencyMs)
+ .append(", logHistorySize=")
+ .append(CONTENT_CAPTURE_OPTIONS.logHistorySize)
+ .append(", disableFlushForViewTreeAppearing=")
+ .append(CONTENT_CAPTURE_OPTIONS.disableFlushForViewTreeAppearing)
+ .append(", enableReceiver=")
+ .append(CONTENT_CAPTURE_OPTIONS.enableReceiver)
+ .append(", contentProtectionOptions=ContentProtectionOptions [")
+ .append("enableReceiver=")
+ .append(CONTENT_CAPTURE_OPTIONS.contentProtectionOptions.enableReceiver)
+ .append(", bufferSize=")
+ .append(CONTENT_CAPTURE_OPTIONS.contentProtectionOptions.bufferSize)
+ .append("], whitelisted=")
+ .append(CONTENT_CAPTURE_OPTIONS.whitelistedComponents)
+ .append(']')
+ .toString();
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void testParcelSerializationDeserialization() {
+ Parcel parcel = Parcel.obtain();
+ CONTENT_CAPTURE_OPTIONS.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ ContentCaptureOptions actual = ContentCaptureOptions.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(actual).isNotNull();
+ assertThat(actual.loggingLevel).isEqualTo(CONTENT_CAPTURE_OPTIONS.loggingLevel);
+ assertThat(actual.maxBufferSize).isEqualTo(CONTENT_CAPTURE_OPTIONS.maxBufferSize);
+ assertThat(actual.idleFlushingFrequencyMs)
+ .isEqualTo(CONTENT_CAPTURE_OPTIONS.idleFlushingFrequencyMs);
+ assertThat(actual.textChangeFlushingFrequencyMs)
+ .isEqualTo(CONTENT_CAPTURE_OPTIONS.textChangeFlushingFrequencyMs);
+ assertThat(actual.logHistorySize).isEqualTo(CONTENT_CAPTURE_OPTIONS.logHistorySize);
+ assertThat(actual.disableFlushForViewTreeAppearing)
+ .isEqualTo(CONTENT_CAPTURE_OPTIONS.disableFlushForViewTreeAppearing);
+ assertThat(actual.enableReceiver).isEqualTo(CONTENT_CAPTURE_OPTIONS.enableReceiver);
+ assertThat(actual.contentProtectionOptions).isNotNull();
+ assertThat(actual.contentProtectionOptions.enableReceiver)
+ .isEqualTo(CONTENT_CAPTURE_OPTIONS.contentProtectionOptions.enableReceiver);
+ assertThat(actual.contentProtectionOptions.bufferSize)
+ .isEqualTo(CONTENT_CAPTURE_OPTIONS.contentProtectionOptions.bufferSize);
+ assertThat(actual.whitelistedComponents)
+ .containsExactlyElementsIn(CONTENT_CAPTURE_OPTIONS.whitelistedComponents);
+ }
+
@NonNull
- private ArraySet<ComponentName> toSet(@Nullable ComponentName... comps) {
+ private static ArraySet<ComponentName> toSet(@Nullable ComponentName... comps) {
ArraySet<ComponentName> set = new ArraySet<>();
if (comps != null) {
for (int i = 0; i < comps.length; i++) {
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
index 27d58b8..f8ebd09 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
@@ -27,6 +27,8 @@
import android.view.autofill.AutofillId;
import android.view.contentcapture.ViewNode.ViewStructureImpl;
+import com.google.common.collect.ImmutableMap;
+
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -37,6 +39,8 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import java.util.Map;
+
/**
* Unit tests for {@link ContentCaptureSession}.
*
@@ -145,6 +149,35 @@
assertThat(session.mInternalNotifyViewTreeEventFinishedCount).isEqualTo(1);
}
+ @Test
+ public void testGetFlushReasonAsString() {
+ int invalidFlushReason = ContentCaptureSession.FLUSH_REASON_LOGIN_DETECTED + 1;
+ Map<Integer, String> expectedMap =
+ new ImmutableMap.Builder<Integer, String>()
+ .put(ContentCaptureSession.FLUSH_REASON_FULL, "FULL")
+ .put(ContentCaptureSession.FLUSH_REASON_VIEW_ROOT_ENTERED, "VIEW_ROOT")
+ .put(ContentCaptureSession.FLUSH_REASON_SESSION_STARTED, "STARTED")
+ .put(ContentCaptureSession.FLUSH_REASON_SESSION_FINISHED, "FINISHED")
+ .put(ContentCaptureSession.FLUSH_REASON_IDLE_TIMEOUT, "IDLE")
+ .put(ContentCaptureSession.FLUSH_REASON_TEXT_CHANGE_TIMEOUT, "TEXT_CHANGE")
+ .put(ContentCaptureSession.FLUSH_REASON_SESSION_CONNECTED, "CONNECTED")
+ .put(ContentCaptureSession.FLUSH_REASON_FORCE_FLUSH, "FORCE_FLUSH")
+ .put(
+ ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARING,
+ "VIEW_TREE_APPEARING")
+ .put(
+ ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED,
+ "VIEW_TREE_APPEARED")
+ .put(ContentCaptureSession.FLUSH_REASON_LOGIN_DETECTED, "LOGIN_DETECTED")
+ .put(invalidFlushReason, "UNKOWN-" + invalidFlushReason)
+ .build();
+
+ expectedMap.forEach(
+ (reason, expected) ->
+ assertThat(ContentCaptureSession.getFlushReasonAsString(reason))
+ .isEqualTo(expected));
+ }
+
// Cannot use @Spy because we need to pass the session id on constructor
private class MyContentCaptureSession extends ContentCaptureSession {
int mInternalNotifyViewTreeEventStartedCount = 0;
diff --git a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
index 93315f1..a4e77f5 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
@@ -41,49 +41,60 @@
private final Context mContext = InstrumentationRegistry.getTargetContext();
+ private final View mView = new View(mContext);
+
+ private final ViewStructureImpl mViewStructure = new ViewStructureImpl(mView);
+
+ private final ViewNode mViewNode = mViewStructure.getNode();
+
@Mock
private HtmlInfo mHtmlInfoMock;
@Test
public void testUnsupportedProperties() {
- View view = new View(mContext);
+ mViewStructure.setChildCount(1);
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- ViewStructureImpl structure = new ViewStructureImpl(view);
- ViewNode node = structure.getNode();
+ mViewStructure.addChildCount(1);
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- structure.setChildCount(1);
- assertThat(node.getChildCount()).isEqualTo(0);
+ assertThat(mViewStructure.newChild(0)).isNull();
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- structure.addChildCount(1);
- assertThat(node.getChildCount()).isEqualTo(0);
+ assertThat(mViewStructure.asyncNewChild(0)).isNull();
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- assertThat(structure.newChild(0)).isNull();
- assertThat(node.getChildCount()).isEqualTo(0);
+ mViewStructure.asyncCommit();
+ assertThat(mViewNode.getChildCount()).isEqualTo(0);
- assertThat(structure.asyncNewChild(0)).isNull();
- assertThat(node.getChildCount()).isEqualTo(0);
+ mViewStructure.setWebDomain("Y U NO SET?");
+ assertThat(mViewNode.getWebDomain()).isNull();
- structure.asyncCommit();
- assertThat(node.getChildCount()).isEqualTo(0);
+ assertThat(mViewStructure.newHtmlInfoBuilder("WHATEVER")).isNull();
- structure.setWebDomain("Y U NO SET?");
- assertThat(node.getWebDomain()).isNull();
+ mViewStructure.setHtmlInfo(mHtmlInfoMock);
+ assertThat(mViewNode.getHtmlInfo()).isNull();
- assertThat(structure.newHtmlInfoBuilder("WHATEVER")).isNull();
+ mViewStructure.setDataIsSensitive(true);
- structure.setHtmlInfo(mHtmlInfoMock);
- assertThat(node.getHtmlInfo()).isNull();
-
- structure.setDataIsSensitive(true);
-
- assertThat(structure.getTempRect()).isNull();
+ assertThat(mViewStructure.getTempRect()).isNull();
// Graphic properties
- structure.setElevation(6.66f);
- assertThat(node.getElevation()).isWithin(1.0e-10f).of(0f);
- structure.setAlpha(66.6f);
- assertThat(node.getAlpha()).isWithin(1.0e-10f).of(1.0f);
- structure.setTransformation(Matrix.IDENTITY_MATRIX);
- assertThat(node.getTransformation()).isNull();
+ mViewStructure.setElevation(6.66f);
+ assertThat(mViewNode.getElevation()).isEqualTo(0f);
+ mViewStructure.setAlpha(66.6f);
+ assertThat(mViewNode.getAlpha()).isEqualTo(1.0f);
+ mViewStructure.setTransformation(Matrix.IDENTITY_MATRIX);
+ assertThat(mViewNode.getTransformation()).isNull();
+ }
+
+ @Test
+ public void testGetSet_textIdEntry() {
+ assertThat(mViewNode.getTextIdEntry()).isNull();
+
+ String expected = "TEXT_ID_ENTRY";
+ mViewNode.setTextIdEntry(expected);
+
+ assertThat(mViewNode.getTextIdEntry()).isEqualTo(expected);
}
}
diff --git a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java
new file mode 100644
index 0000000..1459799
--- /dev/null
+++ b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 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.view.contentprotection;
+
+import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.View;
+import android.view.contentcapture.ContentCaptureEvent;
+import android.view.contentcapture.ViewNode;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for {@link ContentProtectionUtils}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksCoreTests:android.view.contentprotection.ContentProtectionUtilsTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentProtectionUtilsTest {
+
+ private static final String TEXT = "TEST_TEXT";
+
+ private static final ContentCaptureEvent EVENT = createEvent();
+
+ private static final ViewNode VIEW_NODE = new ViewNode();
+
+ private static final ViewNode VIEW_NODE_WITH_TEXT = createViewNodeWithText();
+
+ @Test
+ public void event_getEventText_null() {
+ String actual = ContentProtectionUtils.getEventText(EVENT);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void event_getEventText_notNull() {
+ ContentCaptureEvent event = createEvent();
+ event.setText(TEXT);
+
+ String actual = ContentProtectionUtils.getEventText(event);
+
+ assertThat(actual).isEqualTo(TEXT);
+ }
+
+ @Test
+ public void event_getViewNodeText_null() {
+ String actual = ContentProtectionUtils.getViewNodeText(EVENT);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void event_getViewNodeText_notNull() {
+ ContentCaptureEvent event = createEvent();
+ event.setViewNode(VIEW_NODE_WITH_TEXT);
+
+ String actual = ContentProtectionUtils.getViewNodeText(event);
+
+ assertThat(actual).isEqualTo(TEXT);
+ }
+
+ @Test
+ public void viewNode_getViewNodeText_null() {
+ String actual = ContentProtectionUtils.getViewNodeText(VIEW_NODE);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void viewNode_getViewNodeText_notNull() {
+ String actual = ContentProtectionUtils.getViewNodeText(VIEW_NODE_WITH_TEXT);
+
+ assertThat(actual).isEqualTo(TEXT);
+ }
+
+ private static ContentCaptureEvent createEvent() {
+ return new ContentCaptureEvent(/* sessionId= */ 123, TYPE_SESSION_STARTED);
+ }
+
+ private static ViewNode createViewNodeWithText() {
+ View view = new View(ApplicationProvider.getApplicationContext());
+ ViewStructureImpl viewStructure = new ViewStructureImpl(view);
+ viewStructure.setText(TEXT);
+ return viewStructure.getNode();
+ }
+}
diff --git a/core/tests/coretests/src/android/widget/PointerIconTest.java b/core/tests/coretests/src/android/widget/PointerIconTest.java
new file mode 100644
index 0000000..8e9e1a5
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/PointerIconTest.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.UiThread;
+import android.app.Instrumentation;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PointerIconTest {
+ private Instrumentation mInstrumentation;
+
+ @Before
+ public void setup() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ }
+
+ @Rule
+ public ActivityScenarioRule<PointerIconTestActivity> mActivityScenarioRule =
+ new ActivityScenarioRule<>(PointerIconTestActivity.class);
+
+ @Test
+ @UiThread
+ public void button_mouse_onResolvePointerIcon_returnsTypeHand() {
+ assertOnResolvePointerIconForMouseEvent(R.id.button, PointerIcon.TYPE_HAND);
+ }
+
+ @Test
+ @UiThread
+ public void button_mouse_disabled_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ false, /* clickable */ true,
+ /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void button_mouse_unclickable_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ true, /* clickable */ false,
+ /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void button_stylus_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.button, /* enabled */ true, /* clickable */ true,
+ /* isMouse */ false);
+ }
+
+ @Test
+ @UiThread
+ public void imageButton_mouse_onResolvePointerIconreturnsTypeHand() {
+ assertOnResolvePointerIconForMouseEvent(R.id.imagebutton, PointerIcon.TYPE_HAND);
+ }
+
+ @Test
+ @UiThread
+ public void imageButton_mouse_diabled_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ false,
+ /* clickable */ true, /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void imageButton_mouse_unclickable_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ true,
+ /* clickable */ false, /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void imageButton_stylus_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.imagebutton, /* enabled */ true,
+ /* clickable */ true, /* isMouse */ false);
+ }
+
+ @Test
+ @UiThread
+ public void textView_mouse_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.textview, /* enabled */ true,
+ /* clickable */ true, /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void textView_stylus_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.textview, /* enabled */ true,
+ /* clickable */ true, /* isMouse */ false);
+ }
+
+ @Test
+ @UiThread
+ public void editText_mouse_onResolvePointerIcon_returnsTypeText() {
+ assertOnResolvePointerIconForMouseEvent(R.id.edittext, PointerIcon.TYPE_TEXT);
+ }
+
+ @Test
+ @UiThread
+ public void editText_stylus_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.edittext, /* enabled */ true,
+ /* clickable */ true, /* isMouse */ false);
+ }
+
+ @Test
+ @UiThread
+ public void spinner_mouse_onResolvePointerIcon_returnsTypeHand() {
+ assertOnResolvePointerIconForMouseEvent(R.id.spinner, PointerIcon.TYPE_HAND);
+ }
+
+ @Test
+ @UiThread
+ public void spinner_mouse_disabled_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ false,
+ /* clickable */ true, /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void spinner_mouse_unclickable_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ true,
+ /* clickable */ false, /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void spinner_stylus_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.spinner, /* enabled */ true, /* clickable */ true,
+ /* isMouse */ false);
+ }
+
+ @Test
+ @UiThread
+ public void radialTimePickerView_mouse_onResolvePointerIcon_returnsTypeHand() {
+ assertOnResolvePointerIconForMouseEvent(R.id.timepicker, PointerIcon.TYPE_HAND);
+
+ }
+
+ @Test
+ @UiThread
+ public void radialTimePickerView_mouse_disabled_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.timepicker, /* enabled */ false,
+ /* clickable */ true, /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void radialTimePickerView_stylus_onResolvePointerIcon_returnsNull() {
+ assertOnResolvePointerIconReturnNull(R.id.timepicker, /* enabled */ true,
+ /* clickable */ true, /* isMouse */ false);
+ }
+
+ @Test
+ @UiThread
+ public void calendarView_mouse_onResolvePointerIcon_returnsTypeHand() {
+ assertPointerIconForCalendarView(/* pointerType */ PointerIcon.TYPE_HAND,
+ /* isMouse */ true);
+ }
+
+ @Test
+ @UiThread
+ public void calendarView_stylus_onResolvePointerIcon_returnsNull() {
+ assertPointerIconForCalendarView(/* pointerType */ Integer.MIN_VALUE, /* isMouse */ false);
+ }
+
+ /**
+ * Assert {@link View#onResolvePointerIcon} method for {@link CalendarView}.
+ *
+ * @param pointerType the expected type of the {@link PointerIcon}.
+ * When {@link Integer#MIN_VALUE} is passed, it will verify that the
+ * returned {@link PointerIcon} is null.
+ * @param isMouse if true, mouse events are used to test the given view. Otherwise, it uses
+ * stylus events to test the view.
+ */
+ void assertPointerIconForCalendarView(int pointerType, boolean isMouse) {
+ Calendar calendar = new GregorianCalendar();
+ calendar.set(2023, 0, 1);
+ long time = calendar.getTimeInMillis();
+ mActivityScenarioRule.getScenario().onActivity(activity -> {
+ CalendarView calendarView = activity.findViewById(R.id.calendar);
+ calendarView.setDate(time, /* animate */ false, /* center */true);
+ });
+
+ // Wait for setDate to finish and then verify.
+ mInstrumentation.waitForIdleSync();
+ mActivityScenarioRule.getScenario().onActivity(activity -> {
+ CalendarView calendarView = activity.findViewById(R.id.calendar);
+ Rect bounds = new Rect();
+ calendarView.getBoundsForDate(time, bounds);
+ MotionEvent event = createHoverEvent(isMouse, bounds.centerX(), bounds.centerY());
+ PointerIcon icon = calendarView.onResolvePointerIcon(event, /* pointerIndex */ 0);
+ if (pointerType != Integer.MIN_VALUE) {
+ assertThat(icon.getType()).isEqualTo(pointerType);
+ } else {
+ assertThat(icon).isNull();
+ }
+ });
+ }
+
+ /**
+ * Assert that the given view's {@link View#onResolvePointerIcon(MotionEvent, int)} method
+ * returns a {@link PointerIcon} with the specified pointer type. The passed {@link MotionEvent}
+ * locates at the center of the view.
+ *
+ * @param resId the resource id of the view to be tested.
+ * @param pointerType the expected pointer type. When {@link Integer#MIN_VALUE} is passed, it
+ * will verify that the returned {@link PointerIcon} is null.
+ */
+ public void assertOnResolvePointerIconForMouseEvent(int resId, int pointerType) {
+ mActivityScenarioRule.getScenario().onActivity(activity -> {
+ View view = activity.findViewById(resId);
+ MotionEvent event = createHoverEvent(/* isMouse */ true, /* x */ 0, /* y */ 0);
+ PointerIcon icon = view.onResolvePointerIcon(event, /* pointerIndex */ 0);
+ if (pointerType != Integer.MIN_VALUE) {
+ assertThat(icon.getType()).isEqualTo(pointerType);
+ } else {
+ assertThat(icon).isNull();
+ }
+ });
+ }
+
+ /**
+ * Assert that the given view's {@link View#onResolvePointerIcon(MotionEvent, int)} method
+ * returns a {@link PointerIcon} with the specified pointer type. The passed {@link MotionEvent}
+ * locates at the center of the view.
+ *
+ * @param resId the resource id of the view to be tested.
+ * @param enabled whether the tested view is enabled.
+ * @param clickable whether the tested view is clickable.
+ * @param isMouse if true, mouse events are used to test the given view. Otherwise, it uses
+ * stylus events to test the view.
+ */
+ public void assertOnResolvePointerIconReturnNull(int resId, boolean enabled, boolean clickable,
+ boolean isMouse) {
+ mActivityScenarioRule.getScenario().onActivity(activity -> {
+ View view = activity.findViewById(resId);
+ view.setEnabled(enabled);
+ view.setClickable(clickable);
+ MotionEvent event = createHoverEvent(isMouse, /* x */ 0, /* y */ 0);
+ PointerIcon icon = view.onResolvePointerIcon(event, /* pointerIndex */ 0);
+ assertThat(icon).isNull();
+ });
+ }
+
+
+ /**
+ * Create a hover {@link MotionEvent} for testing.
+ *
+ * @param isMouse if true, a {@link MotionEvent} from mouse is returned. Otherwise,
+ * a {@link MotionEvent} from stylus is returned.
+ * @param x the x coordinate of the returned {@link MotionEvent}
+ * @param y the y coordinate of the returned {@link MotionEvent}
+ */
+ private MotionEvent createHoverEvent(boolean isMouse, int x, int y) {
+ MotionEvent.PointerProperties[] properties = MotionEvent.PointerProperties.createArray(1);
+ properties[0].toolType =
+ isMouse ? MotionEvent.TOOL_TYPE_MOUSE : MotionEvent.TOOL_TYPE_STYLUS;
+
+ MotionEvent.PointerCoords[] coords = MotionEvent.PointerCoords.createArray(1);
+ coords[0].x = x;
+ coords[0].y = y;
+
+ int source = isMouse ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_STYLUS;
+ long eventTime = SystemClock.uptimeMillis();
+ return MotionEvent.obtain(/* downTime */ 0, eventTime, MotionEvent.ACTION_HOVER_MOVE,
+ /* pointerCount */ 1, properties, coords, /* metaState */ 0, /* buttonState */ 0,
+ /* xPrecision */ 1, /* yPrecision */ 1, /* deviceId */ 0, /* edgeFlags */ 0,
+ source, /* flags */ 0);
+ }
+
+}
diff --git a/core/tests/coretests/src/android/widget/PointerIconTestActivity.java b/core/tests/coretests/src/android/widget/PointerIconTestActivity.java
new file mode 100644
index 0000000..d491fb0
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/PointerIconTestActivity.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+public class PointerIconTestActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.pointer_icon_test);
+
+ RadialTimePickerView timePicker = findViewById(R.id.timepicker);
+ // Set the time of TimePicker to 0:00 so that the test is stable.
+ timePicker.setCurrentHour(0);
+ timePicker.setCurrentMinute(0);
+ }
+}
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index be2c27d..3e0e36d 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -181,7 +181,7 @@
// Verify for ON_START state. Activity should be relaunched.
getInstrumentation().runOnMainSync(() -> clientSession.startActivity(r));
- recreateAndVerifyRelaunched(activityThread, activity[0], r, ON_START);
+ recreateAndVerifyRelaunched(activityThread, activity[0], r, ON_PAUSE);
// Verify for ON_RESUME state. Activity should be relaunched.
getInstrumentation().runOnMainSync(() -> clientSession.resumeActivity(r));
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 89f4890..4cedd41 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -308,7 +308,8 @@
forAllTaskContainers(taskContainer -> {
synchronized (mLock) {
- final List<TaskFragmentContainer> containers = taskContainer.mContainers;
+ final List<TaskFragmentContainer> containers =
+ taskContainer.getTaskFragmentContainers();
// Clean up the TaskFragmentContainers by the z-order from the lowest.
for (int i = 0; i < containers.size(); i++) {
final TaskFragmentContainer container = containers.get(i);
@@ -611,8 +612,7 @@
@NonNull TaskContainer taskContainer) {
// Update all TaskFragments in the Task. Make a copy of the list since some may be
// removed on updating.
- final List<TaskFragmentContainer> containers =
- new ArrayList<>(taskContainer.mContainers);
+ final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers();
for (int i = containers.size() - 1; i >= 0; i--) {
final TaskFragmentContainer container = containers.get(i);
// Wait until onTaskFragmentAppeared to update new container.
@@ -1331,7 +1331,8 @@
// Check pending appeared activity first because there can be a delay for the server
// update.
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
+ final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
+ .getTaskFragmentContainers();
for (int j = containers.size() - 1; j >= 0; j--) {
final TaskFragmentContainer container = containers.get(j);
if (container.hasPendingAppearedActivity(activityToken)) {
@@ -1342,7 +1343,8 @@
// Check appeared activity if there is no such pending appeared activity.
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
+ final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
+ .getTaskFragmentContainers();
for (int j = containers.size() - 1; j >= 0; j--) {
final TaskFragmentContainer container = containers.get(j);
if (container.hasAppearedActivity(activityToken)) {
@@ -1418,7 +1420,7 @@
if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) {
removeExistingSecondaryContainers(wct, primaryContainer);
}
- primaryContainer.getTaskContainer().mSplitContainers.add(splitContainer);
+ primaryContainer.getTaskContainer().addSplitContainer(splitContainer);
}
/** Cleanups all the dependencies when the TaskFragment is entering PIP. */
@@ -1430,8 +1432,9 @@
return;
}
final List<SplitContainer> splitsToRemove = new ArrayList<>();
+ final List<SplitContainer> splitContainers = taskContainer.getSplitContainers();
final Set<TaskFragmentContainer> containersToUpdate = new ArraySet<>();
- for (SplitContainer splitContainer : taskContainer.mSplitContainers) {
+ for (SplitContainer splitContainer : splitContainers) {
if (splitContainer.getPrimaryContainer() != container
&& splitContainer.getSecondaryContainer() != container) {
continue;
@@ -1449,7 +1452,7 @@
}
}
container.resetDependencies();
- taskContainer.mSplitContainers.removeAll(splitsToRemove);
+ taskContainer.removeSplitContainers(splitsToRemove);
// If there is any TaskFragment split with the PIP TaskFragment, update their presentations
// since the split is dismissed.
// We don't want to close any of them even if they are dependencies of the PIP TaskFragment.
@@ -1471,7 +1474,7 @@
void removeContainers(@NonNull TaskContainer taskContainer,
@NonNull List<TaskFragmentContainer> containers) {
// Remove all split containers that included this one
- taskContainer.mContainers.removeAll(containers);
+ taskContainer.removeTaskFragmentContainers(containers);
// Marked as a pending removal which will be removed after it is actually removed on the
// server side (#onTaskFragmentVanished).
// In this way, we can keep track of the Task bounds until we no longer have any
@@ -1481,7 +1484,8 @@
// Cleanup any split references.
final List<SplitContainer> containersToRemove = new ArrayList<>();
- for (SplitContainer splitContainer : taskContainer.mSplitContainers) {
+ final List<SplitContainer> splitContainers = taskContainer.getSplitContainers();
+ for (SplitContainer splitContainer : splitContainers) {
if (containersToRemove.contains(splitContainer)) {
// Don't need to check because it has been in the remove list.
continue;
@@ -1492,10 +1496,12 @@
containersToRemove.add(splitContainer);
}
}
- taskContainer.mSplitContainers.removeAll(containersToRemove);
+ taskContainer.removeSplitContainers(containersToRemove);
// Cleanup any dependent references.
- for (TaskFragmentContainer containerToUpdate : taskContainer.mContainers) {
+ final List<TaskFragmentContainer> taskFragmentContainers =
+ taskContainer.getTaskFragmentContainers();
+ for (TaskFragmentContainer containerToUpdate : taskFragmentContainers) {
containerToUpdate.removeContainersToFinishOnExit(containers);
}
}
@@ -1534,8 +1540,9 @@
if (taskContainer == null) {
return null;
}
- for (int i = taskContainer.mContainers.size() - 1; i >= 0; i--) {
- final TaskFragmentContainer container = taskContainer.mContainers.get(i);
+ final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers();
+ for (int i = containers.size() - 1; i >= 0; i--) {
+ final TaskFragmentContainer container = containers.get(i);
if (!container.isFinished() && (container.getRunningActivityCount() > 0
// We may be waiting for the top TaskFragment to become non-empty after
// creation. In that case, we don't want to treat the TaskFragment below it as
@@ -1629,7 +1636,7 @@
/** Whether the given split is the topmost split in the Task. */
private boolean isTopMostSplit(@NonNull SplitContainer splitContainer) {
final List<SplitContainer> splitContainers = splitContainer.getPrimaryContainer()
- .getTaskContainer().mSplitContainers;
+ .getTaskContainer().getSplitContainers();
return splitContainer == splitContainers.get(splitContainers.size() - 1);
}
@@ -1641,7 +1648,8 @@
if (container == null) {
return null;
}
- final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers;
+ final List<SplitContainer> splitContainers =
+ container.getTaskContainer().getSplitContainers();
if (splitContainers.isEmpty()) {
return null;
}
@@ -1665,7 +1673,7 @@
@NonNull TaskFragmentContainer firstContainer,
@NonNull TaskFragmentContainer secondContainer) {
final List<SplitContainer> splitContainers = firstContainer.getTaskContainer()
- .mSplitContainers;
+ .getSplitContainers();
for (int i = splitContainers.size() - 1; i >= 0; i--) {
final SplitContainer splitContainer = splitContainers.get(i);
final TaskFragmentContainer primary = splitContainer.getPrimaryContainer();
@@ -1930,7 +1938,8 @@
@GuardedBy("mLock")
TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) {
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
+ final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
+ .getTaskFragmentContainers();
for (TaskFragmentContainer container : containers) {
if (container.getTaskFragmentToken().equals(fragmentToken)) {
return container;
@@ -1945,7 +1954,7 @@
@GuardedBy("mLock")
SplitContainer getSplitContainer(@NonNull IBinder token) {
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- final List<SplitContainer> containers = mTaskContainers.valueAt(i).mSplitContainers;
+ final List<SplitContainer> containers = mTaskContainers.valueAt(i).getSplitContainers();
for (SplitContainer container : containers) {
if (container.getToken().equals(token)) {
return container;
@@ -2091,7 +2100,7 @@
}
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
- .mContainers;
+ .getTaskFragmentContainers();
for (int j = containers.size() - 1; j >= 0; j--) {
final TaskFragmentContainer container = containers.get(j);
if (!container.hasActivity(activityToken)
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 4b15bb1..4580c98 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -51,11 +51,11 @@
/** Active TaskFragments in this Task. */
@NonNull
- final List<TaskFragmentContainer> mContainers = new ArrayList<>();
+ private final List<TaskFragmentContainer> mContainers = new ArrayList<>();
/** Active split pairs in this Task. */
@NonNull
- final List<SplitContainer> mSplitContainers = new ArrayList<>();
+ private final List<SplitContainer> mSplitContainers = new ArrayList<>();
@NonNull
private final Configuration mConfiguration;
@@ -207,6 +207,53 @@
return false;
}
+ /**
+ * Returns a list of {@link SplitContainer}. Do not modify the containers directly on the
+ * returned list. Use {@link #addSplitContainer} or {@link #removeSplitContainers} instead.
+ */
+ @NonNull
+ List<SplitContainer> getSplitContainers() {
+ return mSplitContainers;
+ }
+
+ void addSplitContainer(@NonNull SplitContainer splitContainer) {
+ mSplitContainers.add(splitContainer);
+ }
+
+ void removeSplitContainers(@NonNull List<SplitContainer> containers) {
+ mSplitContainers.removeAll(containers);
+ }
+
+ void addTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) {
+ mContainers.add(taskFragmentContainer);
+ }
+
+ void addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer) {
+ mContainers.add(index, taskFragmentContainer);
+ }
+
+ void removeTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) {
+ mContainers.remove(taskFragmentContainer);
+ }
+
+ void removeTaskFragmentContainers(@NonNull List<TaskFragmentContainer> taskFragmentContainer) {
+ mContainers.removeAll(taskFragmentContainer);
+ }
+
+ void clearTaskFragmentContainer() {
+ mContainers.clear();
+ }
+
+ /**
+ * Returns a list of {@link TaskFragmentContainer}. Do not modify the containers directly on
+ * the returned list. Use {@link #addTaskFragmentContainer},
+ * {@link #removeTaskFragmentContainer} or other related methods instead.
+ */
+ @NonNull
+ List<TaskFragmentContainer> getTaskFragmentContainers() {
+ return mContainers;
+ }
+
/** Adds the descriptors of split states in this Task to {@code outSplitStates}. */
void getSplitStates(@NonNull List<SplitInfo> outSplitStates) {
for (SplitContainer container : mSplitContainers) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 60be9d1..61df335 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -180,23 +180,25 @@
throw new IllegalArgumentException(
"pairedPrimaryContainer must be in the same Task");
}
- final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer);
- taskContainer.mContainers.add(primaryIndex + 1, this);
+ final int primaryIndex = taskContainer.indexOf(pairedPrimaryContainer);
+ taskContainer.addTaskFragmentContainer(primaryIndex + 1, this);
} else if (pendingAppearedActivity != null) {
// The TaskFragment will be positioned right above the pending appeared Activity. If any
// existing TaskFragment is empty with pending Intent, it is likely that the Activity of
// the pending Intent hasn't been created yet, so the new Activity should be below the
// empty TaskFragment.
- int i = taskContainer.mContainers.size() - 1;
+ final List<TaskFragmentContainer> containers =
+ taskContainer.getTaskFragmentContainers();
+ int i = containers.size() - 1;
for (; i >= 0; i--) {
- final TaskFragmentContainer container = taskContainer.mContainers.get(i);
+ final TaskFragmentContainer container = containers.get(i);
if (!container.isEmpty() || container.getPendingAppearedIntent() == null) {
break;
}
}
- taskContainer.mContainers.add(i + 1, this);
+ taskContainer.addTaskFragmentContainer(i + 1, this);
} else {
- taskContainer.mContainers.add(this);
+ taskContainer.addTaskFragmentContainer(this);
}
if (pendingAppearedActivity != null) {
addPendingAppearedActivity(pendingAppearedActivity);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index ff08782..9e26472 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -183,23 +183,23 @@
// tf2 has running activity so is active.
final TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class);
doReturn(1).when(tf2).getRunningActivityCount();
- taskContainer.mContainers.add(tf2);
+ taskContainer.addTaskFragmentContainer(tf2);
// tf3 is finished so is not active.
final TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class);
doReturn(true).when(tf3).isFinished();
doReturn(false).when(tf3).isWaitingActivityAppear();
- taskContainer.mContainers.add(tf3);
+ taskContainer.addTaskFragmentContainer(tf3);
mSplitController.mTaskContainers.put(TASK_ID, taskContainer);
assertWithMessage("Must return tf2 because tf3 is not active.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
- taskContainer.mContainers.remove(tf3);
+ taskContainer.removeTaskFragmentContainer(tf3);
assertWithMessage("Must return tf2 because tf2 has running activity.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
- taskContainer.mContainers.remove(tf2);
+ taskContainer.removeTaskFragmentContainer(tf2);
assertWithMessage("Must return tf because we are waiting for tf1 to appear.")
.that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
@@ -320,11 +320,11 @@
doReturn(tf).when(splitContainer).getSecondaryContainer();
doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer();
doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule();
- final List<SplitContainer> splitContainers =
- mSplitController.getTaskContainer(TASK_ID).mSplitContainers;
- splitContainers.add(splitContainer);
+ final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+ taskContainer.addSplitContainer(splitContainer);
// Add a mock SplitContainer on top of splitContainer
- splitContainers.add(1, mock(SplitContainer.class));
+ final SplitContainer splitContainer2 = mock(SplitContainer.class);
+ taskContainer.addSplitContainer(splitContainer2);
mSplitController.updateContainer(mTransaction, tf);
@@ -332,7 +332,9 @@
// Verify if one or both containers in the top SplitContainer are finished,
// dismissPlaceholder() won't be called.
- splitContainers.remove(1);
+ final ArrayList<SplitContainer> splitContainersToRemove = new ArrayList<>();
+ splitContainersToRemove.add(splitContainer2);
+ taskContainer.removeSplitContainers(splitContainersToRemove);
doReturn(true).when(tf).isFinished();
mSplitController.updateContainer(mTransaction, tf);
@@ -363,7 +365,8 @@
final Activity r1 = createMockActivity();
addSplitTaskFragments(r0, r1);
final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
- final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0);
+ final TaskFragmentContainer taskFragmentContainer =
+ taskContainer.getTaskFragmentContainers().get(0);
spyOn(taskContainer);
// No update when the Task is invisible.
@@ -377,7 +380,7 @@
doReturn(true).when(taskContainer).isVisible();
mSplitController.updateContainer(mTransaction, taskFragmentContainer);
- verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
+ verify(mSplitPresenter).updateSplitContainer(taskContainer.getSplitContainers().get(0),
mTransaction);
}
@@ -1090,8 +1093,8 @@
verify(mTransaction).finishActivity(mActivity.getActivityToken());
verify(mTransaction).finishActivity(secondaryActivity0.getActivityToken());
verify(mTransaction).finishActivity(secondaryActivity1.getActivityToken());
- assertTrue(taskContainer.mContainers.isEmpty());
- assertTrue(taskContainer.mSplitContainers.isEmpty());
+ assertTrue(taskContainer.getTaskFragmentContainers().isEmpty());
+ assertTrue(taskContainer.getSplitContainers().isEmpty());
}
@Test
@@ -1363,15 +1366,13 @@
TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
tf.setInfo(mTransaction, createMockTaskFragmentInfo(tf, mActivity));
- List<TaskFragmentContainer> containers = mSplitController.mTaskContainers.get(TASK_ID)
- .mContainers;
-
- assertEquals(containers.get(0), tf);
+ final TaskContainer taskContainer = mSplitController.mTaskContainers.get(TASK_ID);
+ assertEquals(taskContainer.getTaskFragmentContainers().get(0), tf);
mSplitController.finishActivityStacks(Collections.singleton(tf.getTaskFragmentToken()));
verify(mSplitPresenter).deleteTaskFragment(any(), eq(tf.getTaskFragmentToken()));
- assertTrue(containers.isEmpty());
+ assertTrue(taskContainer.getTaskFragmentContainers().isEmpty());
}
@Test
@@ -1381,10 +1382,8 @@
bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity));
topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
- List<TaskFragmentContainer> containers = mSplitController.mTaskContainers.get(TASK_ID)
- .mContainers;
-
- assertEquals(containers.size(), 2);
+ final TaskContainer taskContainer = mSplitController.mTaskContainers.get(TASK_ID);
+ assertEquals(taskContainer.getTaskFragmentContainers().size(), 2);
Set<IBinder> activityStackTokens = new ArraySet<>(new IBinder[]{
topTf.getTaskFragmentToken(), bottomTf.getTaskFragmentToken()});
@@ -1403,7 +1402,7 @@
+ "regardless of the order in ActivityStack set",
topTf.getTaskFragmentToken(), fragmentTokens.get(1));
- assertTrue(containers.isEmpty());
+ assertTrue(taskContainer.getTaskFragmentContainers().isEmpty());
}
@Test
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index 13e7092..11af1d1 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -127,7 +127,7 @@
assertFalse(taskContainer.isEmpty());
taskContainer.mFinishedContainer.add(tf.getTaskFragmentToken());
- taskContainer.mContainers.clear();
+ taskContainer.clearTaskFragmentContainer();
assertFalse(taskContainer.isEmpty());
}
@@ -152,13 +152,13 @@
assertNull(taskContainer.getTopNonFinishingActivity());
final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class);
- taskContainer.mContainers.add(tf0);
+ taskContainer.addTaskFragmentContainer(tf0);
final Activity activity0 = mock(Activity.class);
doReturn(activity0).when(tf0).getTopNonFinishingActivity();
assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class);
- taskContainer.mContainers.add(tf1);
+ taskContainer.addTaskFragmentContainer(tf1);
assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
final Activity activity1 = mock(Activity.class);
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml
new file mode 100644
index 0000000..d99d64d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true">
+ <ripple android:color="#99999999">
+ <item android:drawable="@drawable/bubble_manage_menu_bg" />
+ </ripple>
+ </item>
+ <item android:drawable="@drawable/bubble_manage_menu_bg" />
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/ic_expand_less.xml b/libs/WindowManager/Shell/res/drawable/ic_expand_less.xml
new file mode 100644
index 0000000..f450846
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/ic_expand_less.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M18.59,16.41L20,15L12,7L4,15L5.41,16.41L12,9.83"
+ android:fillColor="#5F6368"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml
new file mode 100644
index 0000000..ddcd5c6
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/bubble_bar_manage_menu_item_height"
+ android:gravity="center_vertical"
+ android:paddingStart="@dimen/bubble_menu_padding"
+ android:paddingEnd="@dimen/bubble_menu_padding"
+ android:background="@drawable/bubble_manage_menu_row">
+
+ <ImageView
+ android:id="@+id/bubble_bar_menu_item_icon"
+ android:layout_width="@dimen/bubble_bar_manage_menu_item_icon_size"
+ android:layout_height="@dimen/bubble_bar_manage_menu_item_icon_size"
+ android:contentDescription="@null"/>
+
+ <TextView
+ android:id="@+id/bubble_bar_menu_item_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
+
+</com.android.wm.shell.bubbles.bar.BubbleBarMenuItemView>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
new file mode 100644
index 0000000..82e5aee
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<com.android.wm.shell.bubbles.bar.BubbleBarMenuView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:minWidth="@dimen/bubble_bar_manage_menu_min_width"
+ android:orientation="vertical"
+ android:elevation="@dimen/bubble_manage_menu_elevation"
+ android:paddingTop="@dimen/bubble_bar_manage_menu_padding_top"
+ android:paddingHorizontal="@dimen/bubble_bar_manage_menu_padding"
+ android:paddingBottom="@dimen/bubble_bar_manage_menu_padding"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:id="@+id/bubble_bar_manage_menu_bubble_section"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/bubble_bar_manage_menu_item_height"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingStart="14dp"
+ android:paddingEnd="12dp"
+ android:background="@drawable/bubble_manage_menu_section"
+ android:elevation="@dimen/bubble_manage_menu_elevation">
+
+ <ImageView
+ android:id="@+id/bubble_bar_manage_menu_bubble_icon"
+ android:layout_width="@dimen/bubble_menu_icon_size"
+ android:layout_height="@dimen/bubble_menu_icon_size"
+ android:contentDescription="@null" />
+
+ <TextView
+ android:id="@+id/bubble_bar_manage_menu_bubble_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_weight="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
+
+ <ImageView
+ android:id="@+id/bubble_bar_manage_menu_dismiss_icon"
+ android:layout_width="@dimen/bubble_bar_manage_menu_dismiss_icon_size"
+ android:layout_height="@dimen/bubble_bar_manage_menu_dismiss_icon_size"
+ android:layout_marginStart="8dp"
+ android:contentDescription="@null"
+ android:src="@drawable/ic_expand_less"
+ app:tint="?android:attr/textColorPrimary" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/bubble_bar_manage_menu_actions_section"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginTop="@dimen/bubble_bar_manage_menu_section_spacing"
+ android:background="@drawable/bubble_manage_menu_bg"
+ android:elevation="@dimen/bubble_manage_menu_elevation" />
+
+</com.android.wm.shell.bubbles.bar.BubbleBarMenuView>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 171a6b2..f2a0785 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -30,6 +30,9 @@
<color name="bubbles_light">#FFFFFF</color>
<color name="bubbles_dark">@color/GM2_grey_800</color>
<color name="bubbles_icon_tint">@color/GM2_grey_700</color>
+ <color name="bubble_bar_expanded_view_handle_light">#EBffffff</color>
+ <color name="bubble_bar_expanded_view_handle_dark">#99000000</color>
+ <color name="bubble_bar_expanded_view_menu_close">#DC362E</color>
<!-- PiP -->
<color name="pip_custom_close_bg">#D93025</color>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 2be34c9..2141259 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -229,7 +229,25 @@
<!-- Size of the bubble bar (height), should match transient_taskbar_size in Launcher. -->
<dimen name="bubblebar_size">72dp</dimen>
<!-- The size of the drag handle / menu shown along with a bubble bar expanded view. -->
- <dimen name="bubblebar_expanded_view_menu_size">16dp</dimen>
+ <dimen name="bubble_bar_expanded_view_handle_size">40dp</dimen>
+ <!-- The width of the drag handle shown along with a bubble bar expanded view. -->
+ <dimen name="bubble_bar_expanded_view_handle_width">128dp</dimen>
+ <!-- The height of the drag handle shown along with a bubble bar expanded view. -->
+ <dimen name="bubble_bar_expanded_view_handle_height">4dp</dimen>
+ <!-- Minimum width of the bubble bar manage menu. -->
+ <dimen name="bubble_bar_manage_menu_min_width">200dp</dimen>
+ <!-- Size of the dismiss icon in the bubble bar manage menu. -->
+ <dimen name="bubble_bar_manage_menu_dismiss_icon_size">16dp</dimen>
+ <!-- Padding of the bubble bar manage menu, provides space for menu shadows -->
+ <dimen name="bubble_bar_manage_menu_padding">8dp</dimen>
+ <!-- Top padding of the bubble bar manage menu -->
+ <dimen name="bubble_bar_manage_menu_padding_top">2dp</dimen>
+ <!-- Spacing between sections of the bubble bar manage menu -->
+ <dimen name="bubble_bar_manage_menu_section_spacing">2dp</dimen>
+ <!-- Height of an item in the bubble bar manage menu. -->
+ <dimen name="bubble_bar_manage_menu_item_height">52dp</dimen>
+ <!-- Size of the icons in the bubble bar manage menu. -->
+ <dimen name="bubble_bar_manage_menu_item_icon_size">20dp</dimen>
<!-- Bottom and end margin for compat buttons. -->
<dimen name="compat_button_margin">24dp</dimen>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 102f2cb..504839f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -855,7 +855,8 @@
return mIsAppBubble;
}
- Intent getSettingsIntent(final Context context) {
+ /** Creates open app settings intent */
+ public Intent getSettingsIntent(final Context context) {
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
final int uid = getUid(context);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 3eb9fa2..39338e5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -720,6 +720,7 @@
// TODO(b/273312602): consider foldables where we do need a stack view when folded
if (mLayerView == null) {
mLayerView = new BubbleBarLayerView(mContext, this);
+ mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
} else {
if (mStackView == null) {
@@ -1221,6 +1222,13 @@
}
/**
+ * Dismiss bubble if it exists and remove it from the stack
+ */
+ public void dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason) {
+ mBubbleData.dismissBubbleWithKey(bubble.getKey(), reason);
+ }
+
+ /**
* Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot
* can be access via the supplied {@link ScreenshotSync#get()} asynchronously.
*/
@@ -1846,7 +1854,7 @@
if (mStackView != null) {
mStackView.setVisibility(VISIBLE);
}
- if (mLayerView != null && isStackExpanded()) {
+ if (mLayerView != null) {
mLayerView.setVisibility(VISIBLE);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 7a58159..da4a989 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -15,6 +15,7 @@
*/
package com.android.wm.shell.bubbles;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
@@ -110,6 +111,9 @@
try {
options.setTaskAlwaysOnTop(true);
options.setLaunchedFromBubble(true);
+ options.setPendingIntentBackgroundActivityStartMode(
+ MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
Intent fillInIntent = new Intent();
// Apply flags to make behaviour match documentLaunchMode=always.
@@ -117,11 +121,19 @@
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
if (mBubble.isAppBubble()) {
- PendingIntent pi = PendingIntent.getActivity(mContext, 0,
- mBubble.getAppBubbleIntent(),
- PendingIntent.FLAG_MUTABLE,
- null);
- mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+ Context context =
+ mContext.createContextAsUser(
+ mBubble.getUser(), Context.CONTEXT_RESTRICTED);
+ PendingIntent pi = PendingIntent.getActivity(
+ context,
+ /* requestCode= */ 0,
+ mBubble.getAppBubbleIntent()
+ .addFlags(FLAG_ACTIVITY_NEW_DOCUMENT)
+ .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
+ /* options= */ null);
+ mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
+ launchBounds);
} else if (mBubble.hasMetadataShortcutId()) {
options.setApplyActivityFlagsForBubbles(true);
mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index 8ab9841..3a46797 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -192,6 +192,11 @@
}
info.rawBadgeBitmap = iconFactory.getBadgeBitmap(badgedIcon, false).icon;
+ float[] bubbleBitmapScale = new float[1];
+ info.bubbleBitmap = iconFactory.getBubbleBitmap(
+ iconFactory.getBubbleDrawable(c, info.shortcutInfo,
+ b.getIcon()), bubbleBitmapScale);
+
return info;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index b8f049b..32ed102 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -16,11 +16,14 @@
package com.android.wm.shell.bubbles.bar;
+import android.annotation.ColorInt;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Outline;
+import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
@@ -31,8 +34,12 @@
import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleTaskViewHelper;
+import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.taskview.TaskView;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
/**
* Expanded view of a bubble when it's part of the bubble bar.
*
@@ -45,11 +52,14 @@
private BubbleController mController;
private BubbleTaskViewHelper mBubbleTaskViewHelper;
+ private BubbleBarMenuViewController mMenuViewController;
+ private @Nullable Supplier<Rect> mLayerBoundsSupplier;
+ private @Nullable Consumer<String> mUnBubbleConversationCallback;
- private HandleView mMenuView;
- private TaskView mTaskView;
+ private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext());
+ private @Nullable TaskView mTaskView;
- private int mMenuHeight;
+ private int mHandleHeight;
private int mBackgroundColor;
private float mCornerRadius = 0f;
@@ -83,11 +93,9 @@
super.onFinishInflate();
Context context = getContext();
setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation));
- mMenuHeight = context.getResources().getDimensionPixelSize(
- R.dimen.bubblebar_expanded_view_menu_size);
- mMenuView = new HandleView(context);
- addView(mMenuView);
-
+ mHandleHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.bubble_bar_expanded_view_handle_size);
+ addView(mHandleView);
applyThemeAttrs();
setClipToOutline(true);
setOutlineProvider(new ViewOutlineProvider() {
@@ -98,6 +106,13 @@
});
}
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ // Hide manage menu when view disappears
+ mMenuViewController.hideMenu(false /* animated */);
+ }
+
/** Set the BubbleController on the view, must be called before doing anything else. */
public void initialize(BubbleController controller) {
mController = controller;
@@ -108,13 +123,43 @@
addView(mTaskView);
mTaskView.setEnableSurfaceClipping(true);
mTaskView.setCornerRadius(mCornerRadius);
+ mMenuViewController = new BubbleBarMenuViewController(mContext, this);
+ mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
+ @Override
+ public void onMenuVisibilityChanged(boolean visible) {
+ if (mTaskView == null || mLayerBoundsSupplier == null) return;
+ // Updates the obscured touchable region for the task surface.
+ mTaskView.setObscuredTouchRect(visible ? mLayerBoundsSupplier.get() : null);
+ }
+
+ @Override
+ public void onUnBubbleConversation(Bubble bubble) {
+ if (mUnBubbleConversationCallback != null) {
+ mUnBubbleConversationCallback.accept(bubble.getKey());
+ }
+ }
+
+ @Override
+ public void onOpenAppSettings(Bubble bubble) {
+ mController.collapseStack();
+ mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser());
+ }
+
+ @Override
+ public void onDismissBubble(Bubble bubble) {
+ mController.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED);
+ }
+ });
+ mHandleView.setOnClickListener(view -> {
+ mMenuViewController.showMenu(true /* animated */);
+ });
}
// TODO (b/275087636): call this when theme/config changes
void applyThemeAttrs() {
boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
mContext.getResources());
- final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
android.R.attr.dialogCornerRadius,
android.R.attr.colorBackgroundFloating});
mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
@@ -123,14 +168,12 @@
ta.recycle();
- mMenuView.setCornerRadius(mCornerRadius);
- mMenuHeight = getResources().getDimensionPixelSize(
- R.dimen.bubblebar_expanded_view_menu_size);
+ mHandleHeight = getResources().getDimensionPixelSize(
+ R.dimen.bubble_bar_expanded_view_handle_size);
if (mTaskView != null) {
mTaskView.setCornerRadius(mCornerRadius);
- mTaskView.setElevation(150);
- updateMenuColor();
+ updateHandleAndBackgroundColor(true /* animated */);
}
}
@@ -138,10 +181,8 @@
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
-
- // Add corner radius here so that the menu extends behind the rounded corners of TaskView.
- int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height);
- measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
+ int menuViewHeight = Math.min(mHandleHeight, height);
+ measureChild(mHandleView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
MeasureSpec.getMode(heightMeasureSpec)));
if (mTaskView != null) {
@@ -153,12 +194,12 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
// Drag handle above
- final int dragHandleBottom = t + mMenuView.getMeasuredHeight();
- mMenuView.layout(l, t, r, dragHandleBottom);
+ final int dragHandleBottom = t + mHandleView.getMeasuredHeight();
+ mHandleView.layout(l, t, r, dragHandleBottom);
if (mTaskView != null) {
- // Subtract radius so that the menu extends behind the rounded corners of TaskView.
- mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r,
+ mTaskView.layout(l, dragHandleBottom, r,
dragHandleBottom + mTaskView.getMeasuredHeight());
}
}
@@ -166,7 +207,7 @@
@Override
public void onTaskCreated() {
setContentVisibility(true);
- updateMenuColor();
+ updateHandleAndBackgroundColor(false /* animated */);
}
@Override
@@ -187,11 +228,13 @@
}
mBubbleTaskViewHelper.cleanUpTaskView();
}
+ mMenuViewController.hideMenu(false /* animated */);
}
- /** Updates the bubble shown in this task view. */
+ /** Updates the bubble shown in the expanded view. */
public void update(Bubble bubble) {
mBubbleTaskViewHelper.update(bubble);
+ mMenuViewController.updateMenu(bubble);
}
/** The task id of the activity shown in the task view, if it exists. */
@@ -199,6 +242,17 @@
return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID;
}
+ /** Sets layer bounds supplier used for obscured touchable region of task view */
+ void setLayerBoundsSupplier(@Nullable Supplier<Rect> supplier) {
+ mLayerBoundsSupplier = supplier;
+ }
+
+ /** Sets the function to call to un-bubble the given conversation. */
+ public void setUnBubbleConversationCallback(
+ @Nullable Consumer<String> unBubbleConversationCallback) {
+ mUnBubbleConversationCallback = unBubbleConversationCallback;
+ }
+
/**
* Call when the location or size of the view has changed to update TaskView.
*/
@@ -218,16 +272,33 @@
}
}
- /** Updates the menu bar to be the status bar color specified by the app. */
- private void updateMenuColor() {
+ /**
+ * Updates the background color to match with task view status/bg color, and sets handle color
+ * to contrast with the background
+ */
+ private void updateHandleAndBackgroundColor(boolean animated) {
if (mTaskView == null) return;
- ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo();
- final int taskBgColor = info.taskDescription.getStatusBarColor();
- final int color = Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
- if (color != -1) {
- mMenuView.setBackgroundColor(color);
+ final int color = getTaskViewColor();
+ final boolean isRegionDark = Color.luminance(color) <= 0.5;
+ mHandleView.updateHandleColor(isRegionDark, animated);
+ setBackgroundColor(color);
+ }
+
+ /**
+ * Retrieves task view status/nav bar color or background if available
+ *
+ * TODO (b/283075226): Update with color sampling when
+ * RegionSamplingHelper or alternative is available
+ */
+ private @ColorInt int getTaskViewColor() {
+ if (mTaskView == null || mTaskView.getTaskInfo() == null) return mBackgroundColor;
+ ActivityManager.TaskDescription taskDescription = mTaskView.getTaskInfo().taskDescription;
+ if (taskDescription.getStatusBarColor() != Color.TRANSPARENT) {
+ return taskDescription.getStatusBarColor();
+ } else if (taskDescription.getBackgroundColor() != Color.TRANSPARENT) {
+ return taskDescription.getBackgroundColor();
} else {
- mMenuView.setBackgroundColor(mBackgroundColor);
+ return mBackgroundColor;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
new file mode 100644
index 0000000..ce26bc0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles.bar;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import androidx.annotation.ColorInt;
+import androidx.core.content.ContextCompat;
+
+import com.android.wm.shell.R;
+
+/**
+ * Handle view to show at the top of a bubble bar expanded view.
+ */
+public class BubbleBarHandleView extends View {
+ private static final long COLOR_CHANGE_DURATION = 120;
+
+ private int mHandleWidth;
+ private int mHandleHeight;
+ private @ColorInt int mHandleLightColor;
+ private @ColorInt int mHandleDarkColor;
+ private @Nullable ObjectAnimator mColorChangeAnim;
+
+ public BubbleBarHandleView(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public BubbleBarHandleView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /* defStyleAttr */);
+ }
+
+ public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+ }
+
+ public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mHandleWidth = getResources().getDimensionPixelSize(
+ R.dimen.bubble_bar_expanded_view_handle_width);
+ mHandleHeight = getResources().getDimensionPixelSize(
+ R.dimen.bubble_bar_expanded_view_handle_height);
+ mHandleLightColor = ContextCompat.getColor(getContext(),
+ R.color.bubble_bar_expanded_view_handle_light);
+ mHandleDarkColor = ContextCompat.getColor(getContext(),
+ R.color.bubble_bar_expanded_view_handle_dark);
+
+ setClipToOutline(true);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ final int handleCenterX = view.getWidth() / 2;
+ final int handleCenterY = view.getHeight() / 2;
+ final float handleRadius = mHandleHeight / 2f;
+ Rect handleBounds = new Rect(
+ handleCenterX - mHandleWidth / 2,
+ handleCenterY - mHandleHeight / 2,
+ handleCenterX + mHandleWidth / 2,
+ handleCenterY + mHandleHeight / 2);
+ outline.setRoundRect(handleBounds, handleRadius);
+ }
+ });
+ }
+
+ /**
+ * Updates the handle color.
+ *
+ * @param isRegionDark Whether the background behind the handle is dark, and thus the handle
+ * should be light (and vice versa).
+ * @param animated Whether to animate the change, or apply it immediately.
+ */
+ public void updateHandleColor(boolean isRegionDark, boolean animated) {
+ int newColor = isRegionDark ? mHandleLightColor : mHandleDarkColor;
+ if (mColorChangeAnim != null) {
+ mColorChangeAnim.cancel();
+ }
+ if (animated) {
+ mColorChangeAnim = ObjectAnimator.ofArgb(this, "backgroundColor", newColor);
+ mColorChangeAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mColorChangeAnim = null;
+ }
+ });
+ mColorChangeAnim.setDuration(COLOR_CHANGE_DURATION);
+ mColorChangeAnim.start();
+ } else {
+ setBackgroundColor(newColor);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index b1a725b..bc622e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -32,6 +32,8 @@
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleViewProvider;
+import java.util.function.Consumer;
+
/**
* Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window
* manager to display bubbles. However, it is only used when bubbles are being displayed in
@@ -53,6 +55,7 @@
@Nullable
private BubbleViewProvider mExpandedBubble;
private BubbleBarExpandedView mExpandedView;
+ private @Nullable Consumer<String> mUnBubbleConversationCallback;
// TODO(b/273310265) - currently the view is always on the right, need to update for RTL.
/** Whether the expanded view is displaying on the left of the screen or not. */
@@ -146,6 +149,13 @@
final int width = mPositioner.getExpandedViewWidthForBubbleBar();
final int height = mPositioner.getExpandedViewHeightForBubbleBar();
mExpandedView.setVisibility(GONE);
+ mExpandedView.setUnBubbleConversationCallback(mUnBubbleConversationCallback);
+ mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight()));
+ mExpandedView.setUnBubbleConversationCallback(bubbleKey -> {
+ if (mUnBubbleConversationCallback != null) {
+ mUnBubbleConversationCallback.accept(bubbleKey);
+ }
+ });
addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
}
@@ -165,6 +175,12 @@
showScrim(false);
}
+ /** Sets the function to call to un-bubble the given conversation. */
+ public void setUnBubbleConversationCallback(
+ @Nullable Consumer<String> unBubbleConversationCallback) {
+ mUnBubbleConversationCallback = unBubbleConversationCallback;
+ }
+
/** Updates the expanded view size and position. */
private void updateExpandedView() {
if (mExpandedView == null) return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java
new file mode 100644
index 0000000..00b9777
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles.bar;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Bubble bar expanded view menu item view to display menu action details
+ */
+public class BubbleBarMenuItemView extends LinearLayout {
+ private ImageView mImageView;
+ private TextView mTextView;
+
+ public BubbleBarMenuItemView(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public BubbleBarMenuItemView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /* defStyleAttr */);
+ }
+
+ public BubbleBarMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+ }
+
+ public BubbleBarMenuItemView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mImageView = findViewById(R.id.bubble_bar_menu_item_icon);
+ mTextView = findViewById(R.id.bubble_bar_menu_item_title);
+ }
+
+ /**
+ * Update menu item with the details and tint color
+ */
+ void update(Icon icon, String title, @ColorInt int tint) {
+ if (tint == Color.TRANSPARENT) {
+ final TypedArray typedArray = getContext().obtainStyledAttributes(
+ new int[]{android.R.attr.textColorPrimary});
+ mTextView.setTextColor(typedArray.getColor(0, Color.BLACK));
+ } else {
+ icon.setTint(tint);
+ mTextView.setTextColor(tint);
+ }
+
+ mImageView.setImageIcon(icon);
+ mTextView.setText(title);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
new file mode 100644
index 0000000..211fe0d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles.bar;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.Bubble;
+
+import java.util.ArrayList;
+
+/**
+ * Bubble bar expanded view menu
+ */
+public class BubbleBarMenuView extends LinearLayout {
+ private ViewGroup mBubbleSectionView;
+ private ViewGroup mActionsSectionView;
+ private ImageView mBubbleIconView;
+ private TextView mBubbleTitleView;
+
+ public BubbleBarMenuView(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public BubbleBarMenuView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /* defStyleAttr */);
+ }
+
+ public BubbleBarMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+ }
+
+ public BubbleBarMenuView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mBubbleSectionView = findViewById(R.id.bubble_bar_manage_menu_bubble_section);
+ mActionsSectionView = findViewById(R.id.bubble_bar_manage_menu_actions_section);
+ mBubbleIconView = findViewById(R.id.bubble_bar_manage_menu_bubble_icon);
+ mBubbleTitleView = findViewById(R.id.bubble_bar_manage_menu_bubble_title);
+ }
+
+ /** Update menu details with bubble info */
+ void updateInfo(Bubble bubble) {
+ if (bubble.getIcon() != null) {
+ mBubbleIconView.setImageIcon(bubble.getIcon());
+ } else {
+ mBubbleIconView.setImageBitmap(bubble.getBubbleIcon());
+ }
+ mBubbleTitleView.setText(bubble.getTitle());
+ }
+
+ /**
+ * Update menu action items views
+ * @param actions used to populate menu item views
+ */
+ void updateActions(ArrayList<MenuAction> actions) {
+ mActionsSectionView.removeAllViews();
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+
+ for (MenuAction action : actions) {
+ BubbleBarMenuItemView itemView = (BubbleBarMenuItemView) inflater.inflate(
+ R.layout.bubble_bar_menu_item, mActionsSectionView, false);
+ itemView.update(action.mIcon, action.mTitle, action.mTint);
+ itemView.setOnClickListener(action.mOnClick);
+ mActionsSectionView.addView(itemView);
+ }
+ }
+
+ /** Sets on close menu listener */
+ void setOnCloseListener(Runnable onClose) {
+ mBubbleSectionView.setOnClickListener(view -> {
+ onClose.run();
+ });
+ }
+
+ /**
+ * Overridden to proxy to section views alpha.
+ * @implNote
+ * If animate alpha on the parent (menu container) view, section view shadows get distorted.
+ * To prevent distortion and artifacts alpha changes applied directly on the section views.
+ */
+ @Override
+ public void setAlpha(float alpha) {
+ mBubbleSectionView.setAlpha(alpha);
+ mActionsSectionView.setAlpha(alpha);
+ }
+
+ /**
+ * Overridden to proxy section view alpha value.
+ * @implNote
+ * The assumption is that both section views have the same alpha value
+ */
+ @Override
+ public float getAlpha() {
+ return mBubbleSectionView.getAlpha();
+ }
+
+ /**
+ * Menu action details used to create menu items
+ */
+ static class MenuAction {
+ private Icon mIcon;
+ private @ColorInt int mTint;
+ private String mTitle;
+ private OnClickListener mOnClick;
+
+ MenuAction(Icon icon, String title, OnClickListener onClick) {
+ this(icon, title, Color.TRANSPARENT, onClick);
+ }
+
+ MenuAction(Icon icon, String title, @ColorInt int tint, OnClickListener onClick) {
+ this.mIcon = icon;
+ this.mTitle = title;
+ this.mTint = tint;
+ this.mOnClick = onClick;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
new file mode 100644
index 0000000..8be140c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles.bar;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.core.content.ContextCompat;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.bubbles.Bubble;
+
+import java.util.ArrayList;
+
+/**
+ * Manages bubble bar expanded view menu presentation and animations
+ */
+class BubbleBarMenuViewController {
+ private static final float MENU_INITIAL_SCALE = 0.5f;
+ private final Context mContext;
+ private final ViewGroup mRootView;
+ private @Nullable Listener mListener;
+ private @Nullable Bubble mBubble;
+ private @Nullable BubbleBarMenuView mMenuView;
+ /** A transparent view used to intercept touches to collapse menu when presented */
+ private @Nullable View mScrimView;
+ private @Nullable PhysicsAnimator<BubbleBarMenuView> mMenuAnimator;
+ private PhysicsAnimator.SpringConfig mMenuSpringConfig;
+
+ BubbleBarMenuViewController(Context context, ViewGroup rootView) {
+ mContext = context;
+ mRootView = rootView;
+ mMenuSpringConfig = new PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+ }
+
+ /** Sets menu actions listener */
+ void setListener(@Nullable Listener listener) {
+ mListener = listener;
+ }
+
+ /** Update menu with bubble */
+ void updateMenu(@NonNull Bubble bubble) {
+ mBubble = bubble;
+ }
+
+ /**
+ * Show bubble bar expanded view menu
+ * @param animated if should animate transition
+ */
+ void showMenu(boolean animated) {
+ if (mMenuView == null || mScrimView == null) {
+ setupMenu();
+ }
+ cancelAnimations();
+ mMenuView.setVisibility(View.VISIBLE);
+ mScrimView.setVisibility(View.VISIBLE);
+ Runnable endActions = () -> {
+ mMenuView.getChildAt(0).requestAccessibilityFocus();
+ if (mListener != null) {
+ mListener.onMenuVisibilityChanged(true /* isShown */);
+ }
+ };
+ if (animated) {
+ animateTransition(true /* show */, endActions);
+ } else {
+ endActions.run();
+ }
+ }
+
+ /**
+ * Hide bubble bar expanded view menu
+ * @param animated if should animate transition
+ */
+ void hideMenu(boolean animated) {
+ if (mMenuView == null || mScrimView == null) return;
+ cancelAnimations();
+ Runnable endActions = () -> {
+ mMenuView.setVisibility(View.GONE);
+ mScrimView.setVisibility(View.GONE);
+ if (mListener != null) {
+ mListener.onMenuVisibilityChanged(false /* isShown */);
+ }
+ };
+ if (animated) {
+ animateTransition(false /* show */, endActions);
+ } else {
+ endActions.run();
+ }
+ }
+
+ /**
+ * Animate show/hide menu transition
+ * @param show if should show or hide the menu
+ * @param endActions will be called when animation ends
+ */
+ private void animateTransition(boolean show, Runnable endActions) {
+ if (mMenuView == null) return;
+ mMenuAnimator = PhysicsAnimator.getInstance(mMenuView);
+ mMenuAnimator.setDefaultSpringConfig(mMenuSpringConfig);
+ mMenuAnimator
+ .spring(DynamicAnimation.ALPHA, show ? 1f : 0f)
+ .spring(DynamicAnimation.SCALE_Y, show ? 1f : MENU_INITIAL_SCALE)
+ .withEndActions(() -> {
+ mMenuAnimator = null;
+ endActions.run();
+ })
+ .start();
+ }
+
+ /** Cancel running animations */
+ private void cancelAnimations() {
+ if (mMenuAnimator != null) {
+ mMenuAnimator.cancel();
+ mMenuAnimator = null;
+ }
+ }
+
+ /** Sets up and inflate menu views */
+ private void setupMenu() {
+ // Menu view setup
+ mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate(
+ R.layout.bubble_bar_menu_view, mRootView, false);
+ mMenuView.setAlpha(0f);
+ mMenuView.setPivotY(0f);
+ mMenuView.setScaleY(MENU_INITIAL_SCALE);
+ mMenuView.setOnCloseListener(() -> hideMenu(true /* animated */));
+ if (mBubble != null) {
+ mMenuView.updateInfo(mBubble);
+ mMenuView.updateActions(createMenuActions(mBubble));
+ }
+ // Scrim view setup
+ mScrimView = new View(mContext);
+ mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mScrimView.setOnClickListener(view -> hideMenu(true /* animated */));
+ // Attach to root view
+ mRootView.addView(mScrimView);
+ mRootView.addView(mMenuView);
+ }
+
+ /**
+ * Creates menu actions to populate menu view
+ * @param bubble used to create actions depending on bubble type
+ */
+ private ArrayList<BubbleBarMenuView.MenuAction> createMenuActions(Bubble bubble) {
+ ArrayList<BubbleBarMenuView.MenuAction> menuActions = new ArrayList<>();
+ Resources resources = mContext.getResources();
+
+ if (bubble.isConversation()) {
+ // Don't bubble conversation action
+ menuActions.add(new BubbleBarMenuView.MenuAction(
+ Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble),
+ resources.getString(R.string.bubbles_dont_bubble_conversation),
+ view -> {
+ hideMenu(true /* animated */);
+ if (mListener != null) {
+ mListener.onUnBubbleConversation(bubble);
+ }
+ }
+ ));
+ // Open settings action
+ Icon appIcon = bubble.getRawAppBadge() != null ? Icon.createWithBitmap(
+ bubble.getRawAppBadge()) : null;
+ menuActions.add(new BubbleBarMenuView.MenuAction(
+ appIcon,
+ resources.getString(R.string.bubbles_app_settings, bubble.getAppName()),
+ view -> {
+ hideMenu(true /* animated */);
+ if (mListener != null) {
+ mListener.onOpenAppSettings(bubble);
+ }
+ }
+ ));
+ }
+
+ // Dismiss bubble action
+ menuActions.add(new BubbleBarMenuView.MenuAction(
+ Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow),
+ resources.getString(R.string.bubble_dismiss_text),
+ ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_menu_close),
+ view -> {
+ hideMenu(true /* animated */);
+ if (mListener != null) {
+ mListener.onDismissBubble(bubble);
+ }
+ }
+ ));
+
+ return menuActions;
+ }
+
+ /**
+ * Bubble bar expanded view menu actions listener
+ */
+ interface Listener {
+ /**
+ * Called when manage menu is shown/hidden
+ * If animated will be called when animation ends
+ */
+ void onMenuVisibilityChanged(boolean visible);
+
+ /**
+ * Un-bubbles conversation and removes the bubble from the stack
+ * This conversation will not be bubbled with new messages
+ * @see com.android.wm.shell.bubbles.BubbleController
+ */
+ void onUnBubbleConversation(Bubble bubble);
+
+ /**
+ * Launches app notification bubble settings for the bubble with intent created in:
+ * {@code Bubble.getSettingsIntent}
+ */
+ void onOpenAppSettings(Bubble bubble);
+
+ /**
+ * Dismiss bubble and remove it from the bubble stack
+ */
+ void onDismissBubble(Bubble bubble);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java
deleted file mode 100644
index 9ee8a9d..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.wm.shell.bubbles.bar;
-
-import android.content.Context;
-import android.view.Gravity;
-import android.widget.LinearLayout;
-
-/**
- * Handle / menu view to show at the top of a bubble bar expanded view.
- */
-public class HandleView extends LinearLayout {
-
- // TODO(b/273307221): implement the manage menu in this view.
- public HandleView(Context context) {
- super(context);
- setOrientation(LinearLayout.HORIZONTAL);
- setGravity(Gravity.CENTER);
- }
-
- /**
- * The menu extends past the top of the TaskView because of the rounded corners. This means
- * to center content in the menu we must subtract the radius (i.e. the amount of space covered
- * by TaskView).
- */
- public void setCornerRadius(float radius) {
- setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius);
- }
-}
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 c76937d..ec26800 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
@@ -76,6 +76,9 @@
private int mCurrentHeight;
private AnimatorSet mAnimator;
private boolean mTouching;
+ private boolean mHovering;
+ private final int mHoveringWidth;
+ private final int mHoveringHeight;
public DividerHandleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
@@ -87,6 +90,8 @@
mCurrentHeight = mHeight;
mTouchingWidth = mWidth > mHeight ? mWidth / 2 : mWidth;
mTouchingHeight = mHeight > mWidth ? mHeight / 2 : mHeight;
+ mHoveringWidth = mWidth > mHeight ? ((int) (mWidth * 1.5f)) : mWidth;
+ mHoveringHeight = mHeight > mWidth ? ((int) (mHeight * 1.5f)) : mHeight;
}
/** Sets touching state for this handle view. */
@@ -94,24 +99,32 @@
if (touching == mTouching) {
return;
}
+ setInputState(touching, animate, mTouchingWidth, mTouchingHeight);
+ mTouching = touching;
+ }
+
+ /** Sets hovering state for this handle view. */
+ public void setHovering(boolean hovering, boolean animate) {
+ if (hovering == mHovering) {
+ return;
+ }
+ setInputState(hovering, animate, mHoveringWidth, mHoveringHeight);
+ mHovering = hovering;
+ }
+
+ private void setInputState(boolean stateOn, boolean animate, int stateWidth, int stateHeight) {
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
if (!animate) {
- if (touching) {
- mCurrentWidth = mTouchingWidth;
- mCurrentHeight = mTouchingHeight;
- } else {
- mCurrentWidth = mWidth;
- mCurrentHeight = mHeight;
- }
+ mCurrentWidth = stateOn ? stateWidth : mWidth;
+ mCurrentHeight = stateOn ? stateHeight : mHeight;
invalidate();
} else {
- animateToTarget(touching ? mTouchingWidth : mWidth,
- touching ? mTouchingHeight : mHeight, touching);
+ animateToTarget(stateOn ? stateWidth : mWidth,
+ stateOn ? stateHeight : mHeight, stateOn);
}
- mTouching = touching;
}
private void animateToTarget(int targetWidth, int targetHeight, boolean touching) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index 69f0bad..262d487 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -17,14 +17,19 @@
package com.android.wm.shell.common.split;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
+import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
+import android.provider.DeviceConfig;
import android.util.AttributeSet;
import android.util.Property;
import android.view.GestureDetector;
@@ -32,6 +37,7 @@
import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.MotionEvent;
+import android.view.PointerIcon;
import android.view.SurfaceControlViewHost;
import android.view.VelocityTracker;
import android.view.View;
@@ -46,6 +52,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.DividerSnapAlgorithm;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
@@ -222,7 +229,7 @@
for (int i = insetsState.sourceSize() - 1; i >= 0; i--) {
final InsetsSource source = insetsState.sourceAt(i);
if (source.getType() == WindowInsets.Type.navigationBars()
- && source.insetsRoundedCornerFrame()) {
+ && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
mTempRect.inset(source.calculateVisibleInsets(mTempRect));
}
}
@@ -270,6 +277,12 @@
}
@Override
+ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+ return PointerIcon.getSystemIcon(getContext(),
+ isLandscape() ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW);
+ }
+
+ @Override
public boolean onTouch(View v, MotionEvent event) {
if (mSplitLayout == null || !mInteractive) {
return false;
@@ -371,6 +384,43 @@
mViewHost.relayout(lp);
}
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, CURSOR_HOVER_STATES_ENABLED,
+ /* defaultValue = */ false)) {
+ return false;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
+ setHovering();
+ return true;
+ } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
+ releaseHovering();
+ return true;
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ void setHovering() {
+ mHandle.setHovering(true, true);
+ mHandle.animate()
+ .setInterpolator(Interpolators.TOUCH_RESPONSE)
+ .setDuration(TOUCH_ANIMATION_DURATION)
+ .translationZ(mTouchElevation)
+ .start();
+ }
+
+ @VisibleForTesting
+ void releaseHovering() {
+ mHandle.setHovering(false, true);
+ mHandle.animate()
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+ .translationZ(0)
+ .start();
+ }
+
/**
* Set divider should interactive to user or not.
*
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 4980e49..2dbccac 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
@@ -47,6 +47,7 @@
import com.android.wm.shell.common.TabletopModeController;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.annotations.ShellAnimationThread;
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.desktopmode.DesktopModeController;
@@ -264,8 +265,13 @@
static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler(
ShellInit shellInit,
Transitions transitions,
- WindowDecorViewModel windowDecorViewModel) {
- return new FreeformTaskTransitionHandler(shellInit, transitions, windowDecorViewModel);
+ Context context,
+ WindowDecorViewModel windowDecorViewModel,
+ DisplayController displayController,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellAnimationThread ShellExecutor animExecutor) {
+ return new FreeformTaskTransitionHandler(shellInit, transitions, context,
+ windowDecorViewModel, displayController, mainExecutor, animExecutor);
}
@WMSingleton
@@ -536,9 +542,12 @@
Optional<PipTouchHandler> pipTouchHandlerOptional,
Optional<RecentsTransitionHandler> recentsTransitionHandler,
KeyguardTransitionHandler keyguardTransitionHandler,
+ Optional<DesktopModeController> desktopModeController,
+ Optional<DesktopTasksController> desktopTasksController,
Transitions transitions) {
return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional,
- pipTouchHandlerOptional, recentsTransitionHandler, keyguardTransitionHandler);
+ pipTouchHandlerOptional, recentsTransitionHandler, keyguardTransitionHandler,
+ desktopModeController, desktopTasksController);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index b9d2be2..db6c258 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -33,6 +33,7 @@
import android.app.ActivityManager.RunningTaskInfo;
import android.app.WindowConfiguration;
import android.content.Context;
+import android.content.res.TypedArray;
import android.database.ContentObserver;
import android.graphics.Region;
import android.net.Uri;
@@ -414,6 +415,25 @@
}
/**
+ * Applies the proper surface states (rounded corners) to tasks when desktop mode is active.
+ * This is intended to be used when desktop mode is part of another animation but isn't, itself,
+ * animating.
+ */
+ public void syncSurfaceState(@NonNull TransitionInfo info,
+ SurfaceControl.Transaction finishTransaction) {
+ // Add rounded corners to freeform windows
+ final TypedArray ta = mContext.obtainStyledAttributes(
+ new int[]{android.R.attr.dialogCornerRadius});
+ final int cornerRadius = ta.getDimensionPixelSize(0, 0);
+ ta.recycle();
+ for (TransitionInfo.Change change: info.getChanges()) {
+ if (change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ finishTransaction.setCornerRadius(change.getLeash(), cornerRadius);
+ }
+ }
+ }
+
+ /**
* A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
*/
private final class SettingsObserver extends ContentObserver {
@@ -500,6 +520,11 @@
}
@Override
+ public void showDesktopApp(int taskId) throws RemoteException {
+ // TODO
+ }
+
+ @Override
public int getVisibleTaskCount(int displayId) throws RemoteException {
int[] result = new int[1];
executeRemoteCallWithTaskPermission(mController, "getVisibleTaskCount",
@@ -508,5 +533,20 @@
);
return result[0];
}
+
+ @Override
+ public void stashDesktopApps(int displayId) throws RemoteException {
+ // Stashing of desktop apps not needed. Apps always launch on desktop
+ }
+
+ @Override
+ public void hideStashedDesktopApps(int displayId) throws RemoteException {
+ // Stashing of desktop apps not needed. Apps always launch on desktop
+ }
+
+ @Override
+ public void setTaskListener(IDesktopTaskListener listener) throws RemoteException {
+ // TODO(b/261234402): move visibility from sysui state to listener
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 3ab175d..402bb96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -43,6 +43,7 @@
*/
val activeTasks: ArraySet<Int> = ArraySet(),
val visibleTasks: ArraySet<Int> = ArraySet(),
+ var stashed: Boolean = false
)
// Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
@@ -312,6 +313,33 @@
}
/**
+ * Update stashed status on display with id [displayId]
+ */
+ fun setStashed(displayId: Int, stashed: Boolean) {
+ val data = displayData.getOrCreate(displayId)
+ val oldValue = data.stashed
+ data.stashed = stashed
+ if (oldValue != stashed) {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: mark stashed=%b displayId=%d",
+ stashed,
+ displayId
+ )
+ visibleTasksListeners.forEach { (listener, executor) ->
+ executor.execute { listener.onStashedChanged(displayId, stashed) }
+ }
+ }
+ }
+
+ /**
+ * Check if display with id [displayId] has desktop tasks stashed
+ */
+ fun isStashed(displayId: Int): Boolean {
+ return displayData[displayId]?.stashed ?: false
+ }
+
+ /**
* Defines interface for classes that can listen to changes for active tasks in desktop mode.
*/
interface ActiveTasksListener {
@@ -331,5 +359,11 @@
*/
@JvmDefault
fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {}
+
+ /**
+ * Called when the desktop stashed status changes.
+ */
+ @JvmDefault
+ fun onStashedChanged(displayId: Int, stashed: Boolean) {}
}
}
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 91bb155..d191fcd 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.desktopmode
+import android.R
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
@@ -24,6 +25,7 @@
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
+import android.content.res.TypedArray
import android.graphics.Point
import android.graphics.Rect
import android.graphics.Region
@@ -46,6 +48,7 @@
import com.android.wm.shell.common.ExternalInterfaceBinder
import com.android.wm.shell.common.RemoteCallable
import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.common.SingleInstanceRemoteListener
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.common.annotations.ExternalThread
import com.android.wm.shell.common.annotations.ShellMainThread
@@ -118,6 +121,30 @@
}
}
+ /**
+ * Stash desktop tasks on display with id [displayId].
+ *
+ * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps
+ * launched in this state will be added to the desktop. Existing desktop tasks will be brought
+ * back to front during the launch.
+ */
+ fun stashDesktopApps(displayId: Int) {
+ KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps")
+ desktopModeTaskRepository.setStashed(displayId, true)
+ }
+
+ /**
+ * Clear the stashed state for the given display
+ */
+ fun hideStashedDesktopApps(displayId: Int) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: hideStashedApps displayId=%d",
+ displayId
+ )
+ desktopModeTaskRepository.setStashed(displayId, false)
+ }
+
/** Get number of tasks that are marked as visible */
fun getVisibleTaskCount(displayId: Int): Int {
return desktopModeTaskRepository.getVisibleTaskCount(displayId)
@@ -252,6 +279,11 @@
}
/** Move a task to the front */
+ fun moveTaskToFront(taskId: Int) {
+ shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) }
+ }
+
+ /** Move a task to the front */
fun moveTaskToFront(taskInfo: RunningTaskInfo) {
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
@@ -397,6 +429,11 @@
transition: IBinder,
request: TransitionRequestInfo
): WindowContainerTransaction? {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: handleRequest request=%s",
+ request
+ )
// Check if we should skip handling this transition
val shouldHandleRequest =
when {
@@ -418,43 +455,82 @@
}
val task: RunningTaskInfo = request.triggerTask
- val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
- // Check if we should switch a fullscreen task to freeform
- if (task.windowingMode == WINDOWING_MODE_FULLSCREEN) {
- // If there are any visible desktop tasks, switch the task to freeform
- if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
- KtProtoLog.d(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: switch fullscreen task to freeform on transition" +
- " taskId=%d",
- task.taskId
- )
- return WindowContainerTransaction().also { wct ->
- addMoveToDesktopChanges(wct, task.token)
- }
- }
+ return when {
+ // If display has tasks stashed, handle as stashed launch
+ desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task)
+ // Check if fullscreen task should be updated
+ task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task)
+ // Check if freeform task should be updated
+ task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task)
+ else -> null
}
+ }
- // CHeck if we should switch a freeform task to fullscreen
- if (task.windowingMode == WINDOWING_MODE_FREEFORM) {
- // If no visible desktop tasks, switch this task to freeform as the transition came
- // outside of this controller
- if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
- KtProtoLog.d(
+ /**
+ * Applies the proper surface states (rounded corners) to tasks when desktop mode is active.
+ * This is intended to be used when desktop mode is part of another animation but isn't, itself,
+ * animating.
+ */
+ fun syncSurfaceState(
+ info: TransitionInfo,
+ finishTransaction: SurfaceControl.Transaction
+ ) {
+ // Add rounded corners to freeform windows
+ val ta: TypedArray = context.obtainStyledAttributes(
+ intArrayOf(R.attr.dialogCornerRadius))
+ val cornerRadius = ta.getDimensionPixelSize(0, 0).toFloat()
+ ta.recycle()
+ info.changes
+ .filter { it.taskInfo.windowingMode == WINDOWING_MODE_FREEFORM }
+ .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
+ }
+
+ private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+ val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
+ if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
+ KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: switch freeform task to fullscreen oon transition" +
- " taskId=%d",
+ " taskId=%d",
task.taskId
- )
- return WindowContainerTransaction().also { wct ->
- addMoveToFullscreenChanges(wct, task.token)
- }
+ )
+ return WindowContainerTransaction().also { wct ->
+ addMoveToFullscreenChanges(wct, task.token)
}
}
return null
}
+ private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+ val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
+ if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: switch fullscreen task to freeform on transition" +
+ " taskId=%d",
+ task.taskId
+ )
+ return WindowContainerTransaction().also { wct ->
+ addMoveToDesktopChanges(wct, task.token)
+ }
+ }
+ return null
+ }
+
+ private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: launch apps with stashed on transition taskId=%d",
+ task.taskId
+ )
+ val wct = WindowContainerTransaction()
+ bringDesktopAppsToFront(task.displayId, wct)
+ addMoveToDesktopChanges(wct, task.token)
+ desktopModeTaskRepository.setStashed(task.displayId, false)
+ return wct
+ }
+
private fun addMoveToDesktopChanges(
wct: WindowContainerTransaction,
token: WindowContainerToken
@@ -524,14 +600,16 @@
* Perform checks required on drag end. Move to fullscreen if drag ends in status bar area.
*
* @param taskInfo the task being dragged.
- * @param position position of surface when drag ends
+ * @param position position of surface when drag ends.
+ * @param y the Y position of the motion event.
*/
fun onDragPositioningEnd(
taskInfo: RunningTaskInfo,
- position: Point
+ position: Point,
+ y: Float
) {
val statusBarHeight = getStatusBarHeight(taskInfo)
- if (position.y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
+ if (y <= statusBarHeight && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
moveToFullscreenWithAnimation(taskInfo, position)
}
}
@@ -658,8 +736,46 @@
@BinderThread
private class IDesktopModeImpl(private var controller: DesktopTasksController?) :
IDesktopMode.Stub(), ExternalInterfaceBinder {
+
+ private lateinit var remoteListener:
+ SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
+
+ private val listener: VisibleTasksListener = object : VisibleTasksListener {
+ override fun onVisibilityChanged(displayId: Int, visible: Boolean) {
+ // TODO(b/261234402): move visibility from sysui state to listener
+ remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) }
+ }
+
+ override fun onStashedChanged(displayId: Int, stashed: Boolean) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "IDesktopModeImpl: onStashedChanged stashed=%b display=%d",
+ stashed,
+ displayId
+ )
+ remoteListener.call { l -> l.onStashedChanged(displayId, stashed) }
+ }
+ }
+
+ init {
+ remoteListener =
+ SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
+ controller,
+ { c ->
+ c.desktopModeTaskRepository.addVisibleTasksListener(
+ listener,
+ c.mainExecutor
+ )
+ },
+ { c ->
+ c.desktopModeTaskRepository.removeVisibleTasksListener(listener)
+ }
+ )
+ }
+
/** Invalidates this instance, preventing future calls from updating the controller. */
override fun invalidate() {
+ remoteListener.unregister()
controller = null
}
@@ -670,6 +786,27 @@
) { c -> c.showDesktopApps(displayId) }
}
+ override fun stashDesktopApps(displayId: Int) {
+ ExecutorUtils.executeRemoteCallWithTaskPermission(
+ controller,
+ "stashDesktopApps"
+ ) { c -> c.stashDesktopApps(displayId) }
+ }
+
+ override fun hideStashedDesktopApps(displayId: Int) {
+ ExecutorUtils.executeRemoteCallWithTaskPermission(
+ controller,
+ "hideStashedDesktopApps"
+ ) { c -> c.hideStashedDesktopApps(displayId) }
+ }
+
+ override fun showDesktopApp(taskId: Int) {
+ ExecutorUtils.executeRemoteCallWithTaskPermission(
+ controller,
+ "showDesktopApp"
+ ) { c -> c.moveTaskToFront(taskId) }
+ }
+
override fun getVisibleTaskCount(displayId: Int): Int {
val result = IntArray(1)
ExecutorUtils.executeRemoteCallWithTaskPermission(
@@ -680,6 +817,18 @@
)
return result[0]
}
+
+ override fun setTaskListener(listener: IDesktopTaskListener?) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "IDesktopModeImpl: set task listener=%s",
+ listener ?: "null"
+ )
+ ExecutorUtils.executeRemoteCallWithTaskPermission(
+ controller,
+ "setTaskListener"
+ ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() }
+ }
}
companion object {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index 899d672..ee3a080 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -16,6 +16,8 @@
package com.android.wm.shell.desktopmode;
+import com.android.wm.shell.desktopmode.IDesktopTaskListener;
+
/**
* Interface that is exposed to remote callers to manipulate desktop mode features.
*/
@@ -24,6 +26,18 @@
/** Show apps on the desktop on the given display */
void showDesktopApps(int displayId);
+ /** Stash apps on the desktop to allow launching another app from home screen */
+ void stashDesktopApps(int displayId);
+
+ /** Hide apps that may be stashed */
+ void hideStashedDesktopApps(int displayId);
+
+ /** Bring task with the given id to front */
+ oneway void showDesktopApp(int taskId);
+
/** Get count of visible desktop tasks on the given display */
int getVisibleTaskCount(int displayId);
+
+ /** Set listener that will receive callbacks about updates to desktop tasks */
+ oneway void setTaskListener(IDesktopTaskListener listener);
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
new file mode 100644
index 0000000..39128a8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode;
+
+/**
+ * Allows external processes to register a listener in WMShell to get updates about desktop task
+ * state.
+ */
+interface IDesktopTaskListener {
+
+ /** Desktop task visibility has change. Visible if at least 1 task is visible. */
+ oneway void onVisibilityChanged(int displayId, boolean visible);
+
+ /** Desktop task stashed status has changed. */
+ oneway void onStashedChanged(int displayId, boolean stashed);
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 99922fb..c795a05 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -26,6 +26,12 @@
- Non-text ProtoLogs are not currently supported with the Shell library (you can't view them with
traces in Winscope)
+### Kotlin
+
+Protolog tool does not yet have support for Kotlin code (see [b/168581922](https://b.corp.google.com/issues/168581922)).
+For logging in Kotlin, use the [KtProtoLog](frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt)
+class which has a similar API to the Java ProtoLog class.
+
### Enabling ProtoLog command line logging
Run these commands to enable protologs for both WM Core and WM Shell to print to logcat.
```shell
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 04fc79a..55e34fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -19,9 +19,15 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.WindowConfiguration;
+import android.content.Context;
+import android.graphics.Rect;
import android.os.IBinder;
+import android.util.ArrayMap;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
@@ -31,6 +37,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -39,23 +47,37 @@
import java.util.List;
/**
- * The {@link Transitions.TransitionHandler} that handles freeform task maximizing and restoring
- * transitions.
+ * The {@link Transitions.TransitionHandler} that handles freeform task maximizing, closing, and
+ * restoring transitions.
*/
public class FreeformTaskTransitionHandler
implements Transitions.TransitionHandler, FreeformTaskTransitionStarter {
-
+ private static final int CLOSE_ANIM_DURATION = 400;
+ private final Context mContext;
private final Transitions mTransitions;
private final WindowDecorViewModel mWindowDecorViewModel;
+ private final DisplayController mDisplayController;
+ private final ShellExecutor mMainExecutor;
+ private final ShellExecutor mAnimExecutor;
private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
+ private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
+
public FreeformTaskTransitionHandler(
ShellInit shellInit,
Transitions transitions,
- WindowDecorViewModel windowDecorViewModel) {
+ Context context,
+ WindowDecorViewModel windowDecorViewModel,
+ DisplayController displayController,
+ ShellExecutor mainExecutor,
+ ShellExecutor animExecutor) {
mTransitions = transitions;
+ mContext = context;
mWindowDecorViewModel = windowDecorViewModel;
+ mDisplayController = displayController;
+ mMainExecutor = mainExecutor;
+ mAnimExecutor = animExecutor;
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
shellInit.addInitCallback(this::onInit, this);
}
@@ -103,6 +125,14 @@
@NonNull SurfaceControl.Transaction finishT,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
boolean transitionHandled = false;
+ final ArrayList<Animator> animations = new ArrayList<>();
+ final Runnable onAnimFinish = () -> {
+ if (!animations.isEmpty()) return;
+ mMainExecutor.execute(() -> {
+ mAnimations.remove(transition);
+ finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ });
+ };
for (TransitionInfo.Change change : info.getChanges()) {
if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
continue;
@@ -121,21 +151,45 @@
case WindowManager.TRANSIT_TO_BACK:
transitionHandled |= startMinimizeTransition(transition);
break;
+ case WindowManager.TRANSIT_CLOSE:
+ if (change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ transitionHandled |= startCloseTransition(transition, change,
+ finishT, animations, onAnimFinish);
+ }
+ break;
}
}
-
- mPendingTransitionTokens.remove(transition);
-
if (!transitionHandled) {
return false;
}
-
+ mAnimations.put(transition, animations);
+ // startT must be applied before animations start.
startT.apply();
- mTransitions.getMainExecutor().execute(
- () -> finishCallback.onTransitionFinished(null, null));
+ mAnimExecutor.execute(() -> {
+ for (Animator anim : animations) {
+ anim.start();
+ }
+ });
+ // Run this here in case no animators are created.
+ onAnimFinish.run();
+ mPendingTransitionTokens.remove(transition);
return true;
}
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ArrayList<Animator> animations = mAnimations.get(mergeTarget);
+ if (animations == null) return;
+ mAnimExecutor.execute(() -> {
+ for (Animator anim : animations) {
+ anim.end();
+ }
+ });
+
+ }
+
private boolean startChangeTransition(
IBinder transition,
int type,
@@ -165,6 +219,36 @@
return mPendingTransitionTokens.contains(transition);
}
+ private boolean startCloseTransition(IBinder transition, TransitionInfo.Change change,
+ SurfaceControl.Transaction finishT, ArrayList<Animator> animations,
+ Runnable onAnimFinish) {
+ if (!mPendingTransitionTokens.contains(transition)) return false;
+ int screenHeight = mDisplayController
+ .getDisplayLayout(change.getTaskInfo().displayId).height();
+ ValueAnimator animator = new ValueAnimator();
+ animator.setDuration(CLOSE_ANIM_DURATION)
+ .setFloatValues(0f, 1f);
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ SurfaceControl sc = change.getLeash();
+ finishT.hide(sc);
+ Rect startBounds = new Rect(change.getTaskInfo().configuration.windowConfiguration
+ .getBounds());
+ animator.addUpdateListener(animation -> {
+ t.setPosition(sc, startBounds.left,
+ startBounds.top + (animation.getAnimatedFraction() * screenHeight));
+ t.apply();
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ animations.remove(animator);
+ onAnimFinish.run();
+ }
+ });
+ animations.add(animator);
+ return true;
+ }
+
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 64571e0..5ee5324 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -40,6 +40,9 @@
import android.window.WindowContainerTransactionCallback;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.desktopmode.DesktopModeController;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.phone.PipTouchHandler;
@@ -65,6 +68,8 @@
private RecentsTransitionHandler mRecentsHandler;
private StageCoordinator mSplitHandler;
private final KeyguardTransitionHandler mKeyguardHandler;
+ private DesktopModeController mDesktopModeController;
+ private DesktopTasksController mDesktopTasksController;
private static class MixedTransition {
static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
@@ -81,6 +86,9 @@
/** Keyguard exit/occlude/unocclude transition. */
static final int TYPE_KEYGUARD = 5;
+ /** Recents Transition while in desktop mode. */
+ static final int TYPE_RECENTS_DURING_DESKTOP = 6;
+
/** The default animation for this mixed transition. */
static final int ANIM_TYPE_DEFAULT = 0;
@@ -132,7 +140,9 @@
Optional<SplitScreenController> splitScreenControllerOptional,
Optional<PipTouchHandler> pipTouchHandlerOptional,
Optional<RecentsTransitionHandler> recentsHandlerOptional,
- KeyguardTransitionHandler keyguardHandler) {
+ KeyguardTransitionHandler keyguardHandler,
+ Optional<DesktopModeController> desktopModeControllerOptional,
+ Optional<DesktopTasksController> desktopTasksControllerOptional) {
mPlayer = player;
mKeyguardHandler = keyguardHandler;
if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent()
@@ -149,6 +159,8 @@
if (mRecentsHandler != null) {
mRecentsHandler.addMixer(this);
}
+ mDesktopModeController = desktopModeControllerOptional.orElse(null);
+ mDesktopTasksController = desktopTasksControllerOptional.orElse(null);
}, this);
}
}
@@ -218,7 +230,8 @@
@Override
public Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT) {
- if (mRecentsHandler != null && mSplitHandler.isSplitScreenVisible()) {
+ if (mRecentsHandler != null && (mSplitHandler.isSplitScreenVisible()
+ || DesktopModeStatus.isActive(mPlayer.getContext()))) {
return this;
}
return null;
@@ -233,6 +246,13 @@
MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
mixed.mLeftoversHandler = mRecentsHandler;
mActiveTransitions.add(mixed);
+ } else if (DesktopModeStatus.isActive(mPlayer.getContext())) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+ + "desktop mode is active, so treat it as Mixed.");
+ final MixedTransition mixed = new MixedTransition(
+ MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition);
+ mixed.mLeftoversHandler = mRecentsHandler;
+ mActiveTransitions.add(mixed);
} else {
throw new IllegalStateException("Accepted a recents transition but don't know how to"
+ " handle it");
@@ -306,6 +326,9 @@
} else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
return animateKeyguard(mixed, info, startTransaction, finishTransaction,
finishCallback);
+ } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
+ return animateRecentsDuringDesktop(mixed, info, startTransaction, finishTransaction,
+ finishCallback);
} else {
mActiveTransitions.remove(mixed);
throw new IllegalStateException("Starting mixed animation without a known mixed type? "
@@ -582,6 +605,30 @@
return true;
}
+ private boolean animateRecentsDuringDesktop(@NonNull final MixedTransition mixed,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ boolean consumed = mRecentsHandler.startAnimation(
+ mixed.mTransition, info, startTransaction, finishTransaction, finishCallback);
+ if (!consumed) {
+ return false;
+ }
+ //Sync desktop mode state (proto 1)
+ if (mDesktopModeController != null) {
+ mDesktopModeController.syncSurfaceState(info, finishTransaction);
+ return true;
+ }
+ //Sync desktop mode state (proto 2)
+ if (mDesktopTasksController != null) {
+ mDesktopTasksController.syncSurfaceState(info, finishTransaction);
+ return true;
+ }
+
+ return false;
+ }
+
@Override
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@@ -625,6 +672,9 @@
finishCallback);
} else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+ } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
+ mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+ finishCallback);
} else {
throw new IllegalStateException("Playing a mixed transition with unknown type? "
+ mixed.mType);
@@ -650,6 +700,8 @@
mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
} else if (mixed.mType == MixedTransition.TYPE_KEYGUARD) {
mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
+ } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_DESKTOP) {
+ mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
index f81fc6f..6bba0d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
@@ -110,7 +110,7 @@
for (int i = state.sourceSize() - 1; i >= 0; i--) {
final InsetsSource source = state.sourceAt(i);
if (source.getType() == WindowInsets.Type.navigationBars()
- && source.insetsRoundedCornerFrame()) {
+ && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
return source;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
index a4cf149..21994a9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
@@ -50,11 +50,11 @@
import com.android.wm.shell.unfold.UnfoldAnimationController;
import com.android.wm.shell.unfold.UnfoldBackgroundController;
+import dagger.Lazy;
+
import java.util.Optional;
import java.util.concurrent.Executor;
-import dagger.Lazy;
-
/**
* This helper class contains logic that calculates scaling and cropping parameters
* for the folding/unfolding animation. As an input it receives TaskInfo objects and
@@ -149,7 +149,7 @@
for (int i = state.sourceSize() - 1; i >= 0; i--) {
final InsetsSource source = state.sourceAt(i);
if (source.getType() == WindowInsets.Type.navigationBars()
- && source.insetsRoundedCornerFrame()) {
+ && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
return source;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 39fb793..2267c75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -268,7 +268,10 @@
return false;
}
case MotionEvent.ACTION_MOVE: {
- int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ if (e.findPointerIndex(mDragPointerId) == -1) {
+ mDragPointerId = e.getPointerId(0);
+ }
+ final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
mDragPositioningCallback.onDragPositioningMove(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
mIsDragging = true;
@@ -276,7 +279,10 @@
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
- int dragPointerIdx = e.findPointerIndex(mDragPointerId);
+ if (e.findPointerIndex(mDragPointerId) == -1) {
+ mDragPointerId = e.getPointerId(0);
+ }
+ final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
mDragPositioningCallback.onDragPositioningEnd(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
final boolean wasDragging = mIsDragging;
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 9fd57d7..2fd34d9 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
@@ -385,6 +385,9 @@
case MotionEvent.ACTION_MOVE: {
final DesktopModeWindowDecoration decoration =
mWindowDecorByTaskId.get(mTaskId);
+ if (e.findPointerIndex(mDragPointerId) == -1) {
+ mDragPointerId = e.getPointerId(0);
+ }
final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo,
decoration.mTaskSurface, e.getRawY(dragPointerIdx)));
@@ -395,6 +398,9 @@
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
+ if (e.findPointerIndex(mDragPointerId) == -1) {
+ mDragPointerId = e.getPointerId(0);
+ }
final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
// Position of the task is calculated by subtracting the raw location of the
// motion event (the location of the motion relative to the display) by the
@@ -405,7 +411,7 @@
mDragPositioningCallback.onDragPositioningEnd(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo,
- position));
+ position, e.getRawY()));
final boolean wasDragging = mIsDragging;
mIsDragging = false;
return wasDragging;
@@ -814,8 +820,13 @@
@NonNull DesktopModeWindowDecoration windowDecoration,
@NonNull RunningTaskInfo taskInfo) {
final int screenWidth = mDisplayController.getDisplayLayout(taskInfo.displayId).width();
- final Rect disallowedAreaForEndBounds = new Rect(0, 0, screenWidth,
- getStatusBarHeight(taskInfo.displayId));
+ final Rect disallowedAreaForEndBounds;
+ if (DesktopModeStatus.isProto2Enabled()) {
+ disallowedAreaForEndBounds = new Rect(0, 0, screenWidth,
+ getStatusBarHeight(taskInfo.displayId));
+ } else {
+ disallowedAreaForEndBounds = null;
+ }
if (!DesktopModeStatus.isVeiledResizeEnabled()) {
return new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration,
mDisplayController, disallowedAreaForEndBounds, mDragStartListener,
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 65b5a7a..58644b2 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
@@ -73,8 +73,11 @@
return mResultOfDownAction;
}
case ACTION_MOVE: {
+ if (ev.findPointerIndex(mDragPointerId) == -1) {
+ mDragPointerId = ev.getPointerId(0);
+ }
+ final int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
if (!mIsDragEvent) {
- int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x;
float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y;
// Touches generate noisy moves, so only once the move is past the touch
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 58c78e6..39b9021 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
@@ -55,7 +55,7 @@
private final Rect mRepositionTaskBounds = new Rect();
// If a task move (not resize) finishes in this region, the positioner will not attempt to
// finalize the bounds there using WCT#setBounds
- private final Rect mDisallowedAreaForEndBounds = new Rect();
+ private final Rect mDisallowedAreaForEndBounds;
private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
private int mCtrlType;
@@ -77,7 +77,7 @@
mDesktopWindowDecoration = windowDecoration;
mDisplayController = displayController;
mDragStartListener = dragStartListener;
- mDisallowedAreaForEndBounds.set(disallowedAreaForEndBounds);
+ mDisallowedAreaForEndBounds = new Rect(disallowedAreaForEndBounds);
mTransactionSupplier = supplier;
mTransitions = transitions;
}
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 ac5ff20..67ec6cf 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
@@ -248,6 +248,9 @@
mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + captionHeight + params.mCaptionY;
wct.addInsetsSource(mTaskInfo.token,
mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect);
+ wct.addInsetsSource(mTaskInfo.token,
+ mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(),
+ mCaptionInsetsRect);
} else {
startT.hide(mCaptionContainerSurface);
}
@@ -345,6 +348,8 @@
final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get();
wct.removeInsetsSource(mTaskInfo.token,
mOwner, 0 /* index */, WindowInsets.Type.captionBar());
+ wct.removeInsetsSource(mTaskInfo.token,
+ mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures());
mTaskOrganizer.applyTransaction(wct);
}
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index b6696c7..e382a0f 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -23,14 +23,33 @@
default_applicable_licenses: ["frameworks_base_license"],
}
-android_test {
- name: "WMShellFlickerTests",
+filegroup {
+ name: "WMShellFlickerTestsBase-src",
+ srcs: ["src/com/android/wm/shell/flicker/*.kt"],
+}
+
+filegroup {
+ name: "WMShellFlickerTestsBubbles-src",
+ srcs: ["src/**/bubble/*.kt"],
+}
+
+filegroup {
+ name: "WMShellFlickerTestsPip-src",
+ srcs: ["src/**/pip/*.kt"],
+}
+
+filegroup {
+ name: "WMShellFlickerTestsSplitScreen-src",
srcs: [
- "src/**/*.java",
- "src/**/*.kt",
+ "src/**/splitscreen/*.kt",
+ "src/**/splitscreen/benchmark/*.kt",
],
- manifest: "AndroidManifest.xml",
- test_config: "AndroidTest.xml",
+}
+
+java_defaults {
+ name: "WMShellFlickerTestsDefault",
+ manifest: "manifests/AndroidManifest.xml",
+ test_config_template: "AndroidTestTemplate.xml",
platform_apis: true,
certificate: "platform",
optimize: {
@@ -40,16 +59,70 @@
libs: ["android.test.runner"],
static_libs: [
"androidx.test.ext.junit",
+ "flickertestapplib",
"flickerlib",
- "flickerlib-apphelpers",
"flickerlib-helpers",
- "truth-prebuilt",
- "app-helpers-core",
+ "platform-test-annotations",
+ "wm-flicker-common-app-helpers",
+ "wm-flicker-common-assertions",
"launcher-helper-lib",
"launcher-aosp-tapl",
- "wm-flicker-common-assertions",
- "wm-flicker-common-app-helpers",
- "platform-test-annotations",
- "flickertestapplib",
+ ],
+ data: [
+ ":FlickerTestApp",
+ "trace_config/*",
+ ],
+}
+
+android_test {
+ name: "WMShellFlickerTestsOther",
+ defaults: ["WMShellFlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestOther.xml"],
+ package_name: "com.android.wm.shell.flicker",
+ instrumentation_target_package: "com.android.wm.shell.flicker",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ exclude_srcs: [
+ ":WMShellFlickerTestsBubbles-src",
+ ":WMShellFlickerTestsPip-src",
+ ":WMShellFlickerTestsSplitScreen-src",
+ ],
+}
+
+android_test {
+ name: "WMShellFlickerTestsBubbles",
+ defaults: ["WMShellFlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestBubbles.xml"],
+ package_name: "com.android.wm.shell.flicker.bubbles",
+ instrumentation_target_package: "com.android.wm.shell.flicker.bubbles",
+ srcs: [
+ ":WMShellFlickerTestsBase-src",
+ ":WMShellFlickerTestsBubbles-src",
+ ],
+}
+
+android_test {
+ name: "WMShellFlickerTestsPip",
+ defaults: ["WMShellFlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestPip.xml"],
+ package_name: "com.android.wm.shell.flicker.pip",
+ instrumentation_target_package: "com.android.wm.shell.flicker.pip",
+ srcs: [
+ ":WMShellFlickerTestsBase-src",
+ ":WMShellFlickerTestsPip-src",
+ ],
+}
+
+android_test {
+ name: "WMShellFlickerTestsSplitScreen",
+ defaults: ["WMShellFlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestSplitScreen.xml"],
+ package_name: "com.android.wm.shell.flicker.splitscreen",
+ instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen",
+ srcs: [
+ ":WMShellFlickerTestsBase-src",
+ ":WMShellFlickerTestsSplitScreen-src",
],
}
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
deleted file mode 100644
index b5937ae..0000000
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTest.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright 2020 Google Inc. All Rights Reserved.
- -->
-<configuration description="Runs WindowManager Shell Flicker Tests">
- <option name="test-tag" value="FlickerTests" />
- <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
- <!-- keeps the screen on during tests -->
- <option name="screen-always-on" value="on" />
- <!-- prevents the phone from restarting -->
- <option name="force-skip-system-props" value="true" />
- <!-- set WM tracing verbose level to all -->
- <option name="run-command" value="cmd window tracing level all" />
- <!-- set WM tracing to frame (avoid incomplete states) -->
- <option name="run-command" value="cmd window tracing frame" />
- <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
- <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
- <!-- ensure lock screen mode is swipe -->
- <option name="run-command" value="locksettings set-disabled false" />
- <!-- restart launcher to activate TAPL -->
- <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
- <!-- Ensure output directory is empty at the start -->
- <option name="run-command" value="rm -rf /sdcard/flicker" />
- <!-- Increase trace size: 20mb for WM and 80mb for SF -->
- <option name="run-command" value="cmd window tracing size 20480" />
- <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920" />
- </target_preparer>
- <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
- <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" />
- <option name="run-command" value="settings put system show_touches 1" />
- <option name="run-command" value="settings put system pointer_location 1" />
- <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard" />
- <option name="teardown-command" value="settings delete system show_touches" />
- <option name="teardown-command" value="settings delete system pointer_location" />
- <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural" />
- </target_preparer>
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true"/>
- <option name="test-file-name" value="WMShellFlickerTests.apk"/>
- <option name="test-file-name" value="FlickerTestApp.apk" />
- </target_preparer>
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="package" value="com.android.wm.shell.flicker"/>
- <option name="shell-timeout" value="6600s" />
- <option name="test-timeout" value="6000s" />
- <option name="hidden-api-checks" value="false" />
- </test>
- <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
- <option name="directory-keys" value="/sdcard/flicker" />
- <option name="collect-on-run-ended-only" value="true" />
- <option name="clean-up" value="true" />
- </metrics_collector>
-</configuration>
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
new file mode 100644
index 0000000..ffe6d6a2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Runs WindowManager Shell Flicker Tests {MODULE}">
+ <option name="test-tag" value="FlickerTests" />
+ <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
+ <option name="isolated-storage" value="false" />
+
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- keeps the screen on during tests -->
+ <option name="screen-always-on" value="on" />
+ <!-- prevents the phone from restarting -->
+ <option name="force-skip-system-props" value="true" />
+ <!-- set WM tracing verbose level to all -->
+ <option name="run-command" value="cmd window tracing level all" />
+ <!-- set WM tracing to frame (avoid incomplete states) -->
+ <option name="run-command" value="cmd window tracing frame" />
+ <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
+ <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
+ <!-- ensure lock screen mode is swipe -->
+ <option name="run-command" value="locksettings set-disabled false" />
+ <!-- restart launcher to activate TAPL -->
+ <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
+ <!-- Ensure output directory is empty at the start -->
+ <option name="run-command" value="rm -rf /sdcard/flicker" />
+ <!-- Increase trace size: 20mb for WM and 80mb for SF -->
+ <option name="run-command" value="cmd window tracing size 20480" />
+ <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" />
+ <option name="run-command" value="settings put system show_touches 1" />
+ <option name="run-command" value="settings put system pointer_location 1" />
+ <option name="teardown-command"
+ value="settings delete secure show_ime_with_hard_keyboard" />
+ <option name="teardown-command" value="settings delete system show_touches" />
+ <option name="teardown-command" value="settings delete system pointer_location" />
+ <option name="teardown-command"
+ value="cmd overlay enable com.android.internal.systemui.navbar.gestural" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="{MODULE}.apk"/>
+ <option name="test-file-name" value="FlickerTestApp.apk" />
+ </target_preparer>
+ <!-- Needed for pushing the trace config file -->
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="push-file"
+ key="trace_config.textproto"
+ value="/data/misc/perfetto-traces/trace_config.textproto"
+ />
+ <!--Install the content provider automatically when we push some file in sdcard folder.-->
+ <!--Needed to avoid the installation during the test suite.-->
+ <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="{PACKAGE}"/>
+ <option name="shell-timeout" value="6600s" />
+ <option name="test-timeout" value="6000s" />
+ <option name="hidden-api-checks" value="false" />
+ <option name="device-listeners" value="android.device.collectors.PerfettoListener" />
+ <!-- PerfettoListener related arguments -->
+ <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+ <option name="instrumentation-arg"
+ key="perfetto_config_file"
+ value="trace_config.textproto"
+ />
+ <option name="instrumentation-arg" key="per_run" value="true" />
+ </test>
+ <!-- Needed for pulling the collected trace config on to the host -->
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="pull-pattern-keys" value="perfetto_file_path" />
+ </metrics_collector>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="pull-pattern-keys" value="(\w)+\.winscope" />
+ <option name="pull-pattern-keys" value="(\w)+\.mp4" />
+ <option name="collect-on-run-ended-only" value="false" />
+ <option name="clean-up" value="true" />
+ </metrics_collector>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/sdcard/flicker" />
+ <option name="collect-on-run-ended-only" value="true" />
+ <option name="clean-up" value="true" />
+ </metrics_collector>
+</configuration>
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml
similarity index 100%
rename from libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml
rename to libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifest.xml
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestBubbles.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestBubbles.xml
new file mode 100644
index 0000000..437871f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestBubbles.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.wm.shell.flicker.bubble">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.wm.shell.flicker.bubble"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestOther.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestOther.xml
new file mode 100644
index 0000000..cf642f6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestOther.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.wm.shell.flicker">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.wm.shell.flicker"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestPip.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestPip.xml
new file mode 100644
index 0000000..5a8155a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestPip.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.wm.shell.flicker.pip">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.wm.shell.flicker.pip"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestSplitScreen.xml b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestSplitScreen.xml
new file mode 100644
index 0000000..887d8db
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/manifests/AndroidManifestSplitScreen.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.wm.shell.flicker.splitscreen">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.wm.shell.flicker.splitscreen"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto
new file mode 100644
index 0000000..406ada9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/trace_config/trace_config.textproto
@@ -0,0 +1,75 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# proto-message: TraceConfig
+
+# Enable periodic flushing of the trace buffer into the output file.
+write_into_file: true
+
+# Writes the userspace buffer into the file every 1s.
+file_write_period_ms: 2500
+
+# See b/126487238 - we need to guarantee ordering of events.
+flush_period_ms: 30000
+
+# The trace buffers needs to be big enough to hold |file_write_period_ms| of
+# trace data. The trace buffer sizing depends on the number of trace categories
+# enabled and the device activity.
+
+# RSS events
+buffers: {
+ size_kb: 63488
+ fill_policy: RING_BUFFER
+}
+
+data_sources {
+ config {
+ name: "linux.process_stats"
+ target_buffer: 0
+ # polled per-process memory counters and process/thread names.
+ # If you don't want the polled counters, remove the "process_stats_config"
+ # section, but keep the data source itself as it still provides on-demand
+ # thread/process naming for ftrace data below.
+ process_stats_config {
+ scan_all_processes_on_start: true
+ }
+ }
+}
+
+data_sources: {
+ config {
+ name: "linux.ftrace"
+ ftrace_config {
+ ftrace_events: "ftrace/print"
+ ftrace_events: "task/task_newtask"
+ ftrace_events: "task/task_rename"
+ atrace_categories: "ss"
+ atrace_categories: "wm"
+ atrace_categories: "am"
+ atrace_categories: "aidl"
+ atrace_categories: "input"
+ atrace_categories: "binder_driver"
+ atrace_categories: "sched_process_exit"
+ atrace_apps: "com.android.server.wm.flicker.testapp"
+ atrace_apps: "com.android.systemui"
+ atrace_apps: "com.android.wm.shell.flicker"
+ atrace_apps: "com.android.wm.shell.flicker.other"
+ atrace_apps: "com.android.wm.shell.flicker.bubbles"
+ atrace_apps: "com.android.wm.shell.flicker.pip"
+ atrace_apps: "com.android.wm.shell.flicker.splitscreen"
+ atrace_apps: "com.google.android.apps.nexuslauncher"
+ }
+ }
+}
+
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java
new file mode 100644
index 0000000..d38b848
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.bubbles.bar;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.drawable.ColorDrawable;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.core.content.ContextCompat;
+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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class BubbleBarHandleViewTest extends ShellTestCase {
+ private BubbleBarHandleView mHandleView;
+
+ @Before
+ public void setup() {
+ mHandleView = new BubbleBarHandleView(mContext);
+ }
+
+ @Test
+ public void testUpdateHandleColor_lightBg() {
+ mHandleView.updateHandleColor(false /* isRegionDark */, false /* animated */);
+
+ assertTrue(mHandleView.getClipToOutline());
+ assertTrue(mHandleView.getBackground() instanceof ColorDrawable);
+ ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground();
+ assertEquals(bgDrawable.getColor(),
+ ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_dark));
+ }
+
+ @Test
+ public void testUpdateHandleColor_darkBg() {
+ mHandleView.updateHandleColor(true /* isRegionDark */, false /* animated */);
+
+ assertTrue(mHandleView.getClipToOutline());
+ assertTrue(mHandleView.getBackground() instanceof ColorDrawable);
+ ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground();
+ assertEquals(bgDrawable.getColor(),
+ ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_light));
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
new file mode 100644
index 0000000..9da5ab6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.common.split;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.provider.DeviceConfig;
+import android.view.InputDevice;
+import android.view.InsetsState;
+import android.view.MotionEvent;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayImeController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link DividerView} */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DividerViewTest extends ShellTestCase {
+ private @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks;
+ private @Mock SplitLayout.SplitLayoutHandler mSplitLayoutHandler;
+ private @Mock DisplayImeController mDisplayImeController;
+ private @Mock ShellTaskOrganizer mTaskOrganizer;
+ private SplitLayout mSplitLayout;
+ private DividerView mDividerView;
+
+ @Before
+ @UiThreadTest
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ Configuration configuration = getConfiguration();
+ mSplitLayout = new SplitLayout("TestSplitLayout", mContext, configuration,
+ mSplitLayoutHandler, mCallbacks, mDisplayImeController, mTaskOrganizer,
+ SplitLayout.PARALLAX_NONE);
+ SplitWindowManager splitWindowManager = new SplitWindowManager("TestSplitWindowManager",
+ mContext,
+ configuration, mCallbacks);
+ splitWindowManager.init(mSplitLayout, new InsetsState());
+ mDividerView = spy((DividerView) splitWindowManager.getDividerView());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testHoverDividerView() {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CURSOR_HOVER_STATES_ENABLED,
+ "true", false);
+
+ Rect dividerBounds = mSplitLayout.getDividerBounds();
+ int x = dividerBounds.centerX();
+ int y = dividerBounds.centerY();
+ long downTime = SystemClock.uptimeMillis();
+ mDividerView.onHoverEvent(getMotionEvent(downTime, MotionEvent.ACTION_HOVER_ENTER, x, y));
+
+ verify(mDividerView, times(1)).setHovering();
+
+ mDividerView.onHoverEvent(getMotionEvent(downTime, MotionEvent.ACTION_HOVER_EXIT, x, y));
+
+ verify(mDividerView, times(1)).releaseHovering();
+
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CURSOR_HOVER_STATES_ENABLED,
+ "false", false);
+ }
+
+ private static MotionEvent getMotionEvent(long eventTime, int action, float x, float y) {
+ MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
+ properties.id = 0;
+ properties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+
+ MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+ coords.pressure = 1;
+ coords.size = 1;
+ coords.x = x;
+ coords.y = y;
+
+ return MotionEvent.obtain(eventTime, eventTime, action, 1,
+ new MotionEvent.PointerProperties[]{properties},
+ new MotionEvent.PointerCoords[]{coords}, 0, 0, 1.0f, 1.0f, 0, 0,
+ InputDevice.SOURCE_TOUCHSCREEN, 0);
+ }
+
+ private static Configuration getConfiguration() {
+ final Configuration configuration = new Configuration();
+ configuration.unset();
+ configuration.orientation = ORIENTATION_LANDSCAPE;
+ configuration.windowConfiguration.setRotation(0);
+ configuration.windowConfiguration.setBounds(new Rect(0, 0, 1080, 2160));
+ return configuration;
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 3bc2f0e..17c0463 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -313,6 +313,65 @@
assertThat(tasks.first()).isEqualTo(6)
}
+ @Test
+ fun setStashed_stateIsUpdatedForTheDisplay() {
+ repo.setStashed(DEFAULT_DISPLAY, true)
+ assertThat(repo.isStashed(DEFAULT_DISPLAY)).isTrue()
+ assertThat(repo.isStashed(SECOND_DISPLAY)).isFalse()
+
+ repo.setStashed(DEFAULT_DISPLAY, false)
+ assertThat(repo.isStashed(DEFAULT_DISPLAY)).isFalse()
+ }
+
+ @Test
+ fun setStashed_notifyListener() {
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+ repo.setStashed(DEFAULT_DISPLAY, true)
+ executor.flushAll()
+ assertThat(listener.stashedOnDefaultDisplay).isTrue()
+ assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
+
+ repo.setStashed(DEFAULT_DISPLAY, false)
+ executor.flushAll()
+ assertThat(listener.stashedOnDefaultDisplay).isFalse()
+ assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(2)
+ }
+
+ @Test
+ fun setStashed_secondCallDoesNotNotify() {
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+ repo.setStashed(DEFAULT_DISPLAY, true)
+ repo.setStashed(DEFAULT_DISPLAY, true)
+ executor.flushAll()
+ assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
+ }
+
+ @Test
+ fun setStashed_tracksPerDisplay() {
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+
+ repo.setStashed(DEFAULT_DISPLAY, true)
+ executor.flushAll()
+ assertThat(listener.stashedOnDefaultDisplay).isTrue()
+ assertThat(listener.stashedOnSecondaryDisplay).isFalse()
+
+ repo.setStashed(SECOND_DISPLAY, true)
+ executor.flushAll()
+ assertThat(listener.stashedOnDefaultDisplay).isTrue()
+ assertThat(listener.stashedOnSecondaryDisplay).isTrue()
+
+ repo.setStashed(DEFAULT_DISPLAY, false)
+ executor.flushAll()
+ assertThat(listener.stashedOnDefaultDisplay).isFalse()
+ assertThat(listener.stashedOnSecondaryDisplay).isTrue()
+ }
+
class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
@@ -332,6 +391,12 @@
var visibleChangesOnDefaultDisplay = 0
var visibleChangesOnSecondaryDisplay = 0
+ var stashedOnDefaultDisplay = false
+ var stashedOnSecondaryDisplay = false
+
+ var stashedChangesOnDefaultDisplay = 0
+ var stashedChangesOnSecondaryDisplay = 0
+
override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {
when (displayId) {
DEFAULT_DISPLAY -> {
@@ -345,6 +410,20 @@
else -> fail("Visible task listener received unexpected display id: $displayId")
}
}
+
+ override fun onStashedChanged(displayId: Int, stashed: Boolean) {
+ when (displayId) {
+ DEFAULT_DISPLAY -> {
+ stashedOnDefaultDisplay = stashed
+ stashedChangesOnDefaultDisplay++
+ }
+ SECOND_DISPLAY -> {
+ stashedOnSecondaryDisplay = stashed
+ stashedChangesOnDefaultDisplay++
+ }
+ else -> fail("Visible task listener received unexpected display id: $displayId")
+ }
+ }
}
companion object {
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 1335ebf..e3dd449 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
@@ -345,6 +345,18 @@
}
@Test
+ fun moveTaskToFront_postsWctWithReorderOp() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+
+ controller.moveTaskToFront(task1)
+
+ val wct = getLatestWct(expectTransition = TRANSIT_TO_FRONT)
+ assertThat(wct.hierarchyOps).hasSize(1)
+ wct.assertReorderAt(index = 0, task1)
+ }
+
+ @Test
fun moveToNextDisplay_noOtherDisplays() {
whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -451,6 +463,27 @@
}
@Test
+ fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
+ markTaskHidden(stashedFreeformTask)
+
+ val fullscreenTask = createFullscreenTask(DEFAULT_DISPLAY)
+
+ controller.stashDesktopApps(DEFAULT_DISPLAY)
+
+ val result = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+ assertThat(result).isNotNull()
+ result!!.assertReorderSequence(stashedFreeformTask, fullscreenTask)
+ assertThat(result.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+
+ // Stashed state should be cleared
+ assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
+ }
+
+ @Test
fun handleRequest_freeformTask_freeformVisible_returnNull() {
assumeTrue(ENABLE_SHELL_TRANSITIONS)
@@ -501,6 +534,25 @@
}
@Test
+ fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
+ markTaskHidden(stashedFreeformTask)
+
+ val freeformTask = createFreeformTask(DEFAULT_DISPLAY)
+
+ controller.stashDesktopApps(DEFAULT_DISPLAY)
+
+ val result = controller.handleRequest(Binder(), createTransition(freeformTask))
+ assertThat(result).isNotNull()
+ result?.assertReorderSequence(stashedFreeformTask, freeformTask)
+
+ // Stashed state should be cleared
+ assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
+ }
+
+ @Test
fun handleRequest_notOpenOrToFrontTransition_returnNull() {
assumeTrue(ENABLE_SHELL_TRANSITIONS)
@@ -539,6 +591,25 @@
assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
}
+ @Test
+ fun stashDesktopApps_stateUpdates() {
+ controller.stashDesktopApps(DEFAULT_DISPLAY)
+
+ assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue()
+ assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse()
+ }
+
+ @Test
+ fun hideStashedDesktopApps_stateUpdates() {
+ desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true)
+ desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true)
+ controller.hideStashedDesktopApps(DEFAULT_DISPLAY)
+
+ assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
+ // Check that second display is not affected
+ assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue()
+ }
+
private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
val task = createFreeformTask(displayId)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 5a2326b..f941e95 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -316,7 +316,8 @@
releaseOrder.verify(t).remove(captionContainerSurface);
releaseOrder.verify(t).remove(decorContainerSurface);
releaseOrder.verify(t).apply();
- verify(mMockWindowContainerTransaction)
+ // Expect to remove two insets sources, the caption insets and the mandatory gesture insets.
+ verify(mMockWindowContainerTransaction, Mockito.times(2))
.removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt());
}
diff --git a/packages/CarrierDefaultApp/assets/slice_purchase_test.html b/packages/CarrierDefaultApp/assets/slice_purchase_test.html
index 67d2184..917276b 100644
--- a/packages/CarrierDefaultApp/assets/slice_purchase_test.html
+++ b/packages/CarrierDefaultApp/assets/slice_purchase_test.html
@@ -20,7 +20,7 @@
<meta charset="UTF-8">
<meta name="description" content="
This is a HTML page that calls and verifies responses from the @JavascriptInterface functions of
- SlicePurchaseWebInterface. Test slice purchase application behavior using ADB shell commands and
+ DataBoostWebServiceFlow. Test slice purchase application behavior using ADB shell commands and
the APIs below:
FROM TERMINAL:
@@ -65,8 +65,8 @@
<p id="requested_capability"></p>
<h2>Notify purchase successful</h2>
- <button type="button" onclick="testNotifyPurchaseSuccessful(60000)">
- Notify purchase successful for 1 minute
+ <button type="button" onclick="testNotifyPurchaseSuccessful()">
+ Notify purchase successful
</button>
<p id="purchase_successful"></p>
@@ -75,5 +75,11 @@
Notify purchase failed
</button>
<p id="purchase_failed"></p>
+
+ <h2>Dismiss flow</h2>
+ <button type="button" onclick="testDismissFlow()">
+ Dismiss flow
+ </button>
+ <p id="dismiss_flow"></p>
</body>
</html>
diff --git a/packages/CarrierDefaultApp/assets/slice_purchase_test.js b/packages/CarrierDefaultApp/assets/slice_purchase_test.js
index 02c4fea..be397a1 100644
--- a/packages/CarrierDefaultApp/assets/slice_purchase_test.js
+++ b/packages/CarrierDefaultApp/assets/slice_purchase_test.js
@@ -15,19 +15,25 @@
*/
function testGetRequestedCapability() {
- let capability = SlicePurchaseWebInterface.getRequestedCapability();
+ let capability = DataBoostWebServiceFlow.getRequestedCapability();
document.getElementById("requested_capability").innerHTML =
"Premium capability requested: " + capability;
}
-function testNotifyPurchaseSuccessful(duration_ms_long = 0) {
- SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration_ms_long);
+function testNotifyPurchaseSuccessful() {
+ DataBoostWebServiceFlow.notifyPurchaseSuccessful();
document.getElementById("purchase_successful").innerHTML =
- "Notified purchase success for duration: " + duration_ms_long;
+ "Notified purchase successful.";
}
function testNotifyPurchaseFailed(failure_code = 0, failure_reason = "unknown") {
- SlicePurchaseWebInterface.notifyPurchaseFailed(failure_code, failure_reason);
+ DataBoostWebServiceFlow.notifyPurchaseFailed(failure_code, failure_reason);
document.getElementById("purchase_failed").innerHTML =
"Notified purchase failed.";
}
+
+function testDismissFlow() {
+ DataBoostWebServiceFlow.dismissFlow();
+ document.getElementById("dismiss_flow").innerHTML =
+ "Called dismiss flow.";
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/DataBoostWebServiceFlow.java
similarity index 71%
rename from packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java
rename to packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/DataBoostWebServiceFlow.java
index 8547898..4500a22 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseWebInterface.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/DataBoostWebServiceFlow.java
@@ -24,13 +24,13 @@
import com.android.phone.slice.SlicePurchaseController;
/**
- * Slice purchase web interface class allowing carrier websites to send responses back to the
+ * Data boost web service flow interface allowing carrier websites to send responses back to the
* slice purchase application using JavaScript.
*/
-public class SlicePurchaseWebInterface {
+public class DataBoostWebServiceFlow {
@NonNull SlicePurchaseActivity mActivity;
- public SlicePurchaseWebInterface(@NonNull SlicePurchaseActivity activity) {
+ public DataBoostWebServiceFlow(@NonNull SlicePurchaseActivity activity) {
mActivity = activity;
}
@@ -41,7 +41,7 @@
* This can be called using the JavaScript below:
* <script type="text/javascript">
* function getRequestedCapability(duration) {
- * SlicePurchaseWebInterface.getRequestedCapability();
+ * DataBoostWebServiceFlow.getRequestedCapability();
* }
* </script>
*/
@@ -57,16 +57,14 @@
*
* This can be called using the JavaScript below:
* <script type="text/javascript">
- * function notifyPurchaseSuccessful(duration_ms_long = 0) {
- * SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration_ms_long);
+ * function notifyPurchaseSuccessful() {
+ * DataBoostWebServiceFlow.notifyPurchaseSuccessful();
* }
* </script>
- *
- * @param duration The duration for which the premium capability is purchased in milliseconds.
*/
@JavascriptInterface
- public void notifyPurchaseSuccessful(long duration) {
- mActivity.onPurchaseSuccessful(duration);
+ public void notifyPurchaseSuccessful() {
+ mActivity.onPurchaseSuccessful();
}
/**
@@ -76,7 +74,7 @@
* This can be called using the JavaScript below:
* <script type="text/javascript">
* function notifyPurchaseFailed(failure_code = 0, failure_reason = "unknown") {
- * SlicePurchaseWebInterface.notifyPurchaseFailed();
+ * DataBoostWebServiceFlow.notifyPurchaseFailed();
* }
* </script>
*
@@ -90,4 +88,21 @@
@Nullable String failureReason) {
mActivity.onPurchaseFailed(failureCode, failureReason);
}
+
+ /**
+ * Interface method allowing the carrier website to notify the slice purchase application that
+ * the service flow ended prematurely. This can be due to user action, an error in the
+ * web sheet logic, or an error on the network side.
+ *
+ * This can be called using the JavaScript below:
+ * <script type="text/javascript">
+ * function dismissFlow() {
+ * DataBoostWebServiceFlow.dismissFlow();
+ * }
+ * </script>
+ */
+ @JavascriptInterface
+ public void dismissFlow() {
+ mActivity.onDismissFlow();
+ }
}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
index 946185a..2530257d6 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseActivity.java
@@ -27,26 +27,25 @@
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
+import android.webkit.CookieManager;
import android.webkit.WebView;
import com.android.phone.slice.SlicePurchaseController;
import java.net.URL;
-import java.util.concurrent.TimeUnit;
/**
* Activity that launches when the user clicks on the performance boost notification.
* This will open a {@link WebView} for the carrier website to allow the user to complete the
* premium capability purchase.
* The carrier website can get the requested premium capability using the JavaScript interface
- * method {@code SlicePurchaseWebInterface.getRequestedCapability()}.
+ * method {@code DataBoostWebServiceFlow.getRequestedCapability()}.
* If the purchase is successful, the carrier website shall notify the slice purchase application
* using the JavaScript interface method
- * {@code SlicePurchaseWebInterface.notifyPurchaseSuccessful(duration)}, where {@code duration} is
- * the optional duration of the performance boost.
+ * {@code DataBoostWebServiceFlow.notifyPurchaseSuccessful()}.
* If the purchase was not successful, the carrier website shall notify the slice purchase
* application using the JavaScript interface method
- * {@code SlicePurchaseWebInterface.notifyPurchaseFailed(code, reason)}, where {@code code} is the
+ * {@code DataBoostWebServiceFlow.notifyPurchaseFailed(code, reason)}, where {@code code} is the
* {@link SlicePurchaseController.FailureCode} indicating the reason for failure and {@code reason}
* is the human-readable reason for failure if the failure code is
* {@link SlicePurchaseController#FAILURE_CODE_UNKNOWN}.
@@ -118,15 +117,11 @@
setupWebView();
}
- protected void onPurchaseSuccessful(long duration) {
+ protected void onPurchaseSuccessful() {
logd("onPurchaseSuccessful: Carrier website indicated successfully purchased premium "
- + "capability " + TelephonyManager.convertPremiumCapabilityToString(mCapability)
- + (duration > 0 ? " for " + TimeUnit.MILLISECONDS.toMinutes(duration) + " minutes."
- : "."));
- Intent intent = new Intent();
- intent.putExtra(SlicePurchaseController.EXTRA_PURCHASE_DURATION, duration);
- SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponseWithData(mApplicationContext,
- mIntent, SlicePurchaseController.EXTRA_INTENT_SUCCESS, intent);
+ + "capability " + TelephonyManager.convertPremiumCapabilityToString(mCapability));
+ SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
+ mIntent, SlicePurchaseController.EXTRA_INTENT_SUCCESS);
finishAndRemoveTask();
}
@@ -143,6 +138,14 @@
finishAndRemoveTask();
}
+ protected void onDismissFlow() {
+ logd("onDismissFlow: Dismiss flow called while purchasing premium capability "
+ + TelephonyManager.convertPremiumCapabilityToString(mCapability));
+ SlicePurchaseBroadcastReceiver.sendSlicePurchaseAppResponse(
+ mIntent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
+ finishAndRemoveTask();
+ }
+
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
// Pressing back in the WebView will go to the previous page instead of closing
@@ -174,11 +177,17 @@
// Create WebView
mWebView = new WebView(this);
+ // Clear any cookies and state that might be saved from previous sessions
+ CookieManager.getInstance().removeAllCookies(null);
+ CookieManager.getInstance().flush();
+ mWebView.clearCache(true);
+ mWebView.clearHistory();
+
// Enable JavaScript for the carrier purchase website to send results back to
// the slice purchase application.
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(
- new SlicePurchaseWebInterface(this), "SlicePurchaseWebInterface");
+ new DataBoostWebServiceFlow(this), "DataBoostWebServiceFlow");
// Display WebView
setContentView(mWebView);
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
index daf0b42..cc103fa 100644
--- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseActivityTest.java
@@ -53,7 +53,9 @@
private static final int PHONE_ID = 0;
@Mock PendingIntent mPendingIntent;
+ @Mock PendingIntent mSuccessfulIntent;
@Mock PendingIntent mCanceledIntent;
+ @Mock PendingIntent mRequestFailedIntent;
@Mock CarrierConfigManager mCarrierConfigManager;
@Mock NotificationManager mNotificationManager;
@Mock PersistableBundle mPersistableBundle;
@@ -107,25 +109,28 @@
doReturn(true).when(mCanceledIntent).isBroadcast();
doReturn(mCanceledIntent).when(spiedIntent).getParcelableExtra(
eq(SlicePurchaseController.EXTRA_INTENT_CANCELED), eq(PendingIntent.class));
+ doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mSuccessfulIntent).getCreatorPackage();
+ doReturn(true).when(mSuccessfulIntent).isBroadcast();
+ doReturn(mSuccessfulIntent).when(spiedIntent).getParcelableExtra(
+ eq(SlicePurchaseController.EXTRA_INTENT_SUCCESS), eq(PendingIntent.class));
+ doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mRequestFailedIntent)
+ .getCreatorPackage();
+ doReturn(true).when(mRequestFailedIntent).isBroadcast();
+ doReturn(mRequestFailedIntent).when(spiedIntent).getParcelableExtra(
+ eq(SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED), eq(PendingIntent.class));
mSlicePurchaseActivity = startActivity(spiedIntent, null, null);
}
@Test
public void testOnPurchaseSuccessful() throws Exception {
- int duration = 5 * 60 * 1000; // 5 minutes
- int invalidDuration = -1;
- mSlicePurchaseActivity.onPurchaseSuccessful(duration);
- ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
- verify(mPendingIntent).send(eq(mContext), eq(0), intentCaptor.capture());
- Intent intent = intentCaptor.getValue();
- assertEquals(duration, intent.getLongExtra(
- SlicePurchaseController.EXTRA_PURCHASE_DURATION, invalidDuration));
+ mSlicePurchaseActivity.onPurchaseSuccessful();
+ verify(mSuccessfulIntent).send();
}
@Test
public void testOnPurchaseFailed() throws Exception {
- int failureCode = SlicePurchaseController.FAILURE_CODE_SERVER_UNREACHABLE;
+ int failureCode = SlicePurchaseController.FAILURE_CODE_CARRIER_URL_UNAVAILABLE;
String failureReason = "Server unreachable";
mSlicePurchaseActivity.onPurchaseFailed(failureCode, failureReason);
ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -142,4 +147,10 @@
mSlicePurchaseActivity.onDestroy();
verify(mCanceledIntent).send();
}
+
+ @Test
+ public void testOnDismissFlow() throws Exception {
+ mSlicePurchaseActivity.onDismissFlow();
+ verify(mRequestFailedIntent).send();
+ }
}
diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
index aae30df..a0b3469 100644
--- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
+++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
@@ -144,7 +144,7 @@
android:visibility="gone"
android:duplicateParentState="true"
android:clickable="false"
- android:text="@string/consent_no" />
+ android:text="@string/consent_cancel" />
</LinearLayout>
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 2502bbf..5398579 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -28,13 +28,13 @@
<string name="profile_name_watch">watch</string>
<!-- Title of the device selection dialog. -->
- <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by <strong><xliff:g id="app_name" example="Android Wear">%2$s</xliff:g></strong></string>
+ <string name="chooser_title_non_profile">Choose a device to be managed by <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong></string>
- <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_watch">This app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to sync info, like the name of someone calling, interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string>
+ <!-- Tile of the multiple devices' dialog. -->
+ <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to set up</string>
- <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile for singleDevice(type) [CHAR LIMIT=NONE] -->
- <string name="summary_watch_single_device">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string>
+ <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile [CHAR LIMIT=NONE] -->
+ <string name="summary_watch">This app will be allowed to sync info, like the name of someone calling, and access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string>
<!-- ================= DEVICE_PROFILE_GLASSES ================= -->
@@ -42,13 +42,10 @@
<string name="confirmation_title_glasses">Allow <strong><xliff:g id="app_name" example="Android Wear">%1$s</xliff:g></strong> to manage <strong><xliff:g id="device_name" example="Glasses">%2$s</xliff:g></strong>?</string>
<!-- The name of the "glasses" device type [CHAR LIMIT=30] -->
- <string name="profile_name_glasses">glasses</string>
+ <string name="profile_name_glasses">device</string>
- <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_glasses_multi_device">This app is needed to manage <xliff:g id="device_name" example="My Glasses">%1$s</xliff:g>. <xliff:g id="app_name" example="Glasses">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Microphone and Nearby devices permissions.</string>
-
- <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile for singleDevice(type) [CHAR LIMIT=NONE] -->
- <string name="summary_glasses_single_device">This app will be allowed to access these permissions on your <xliff:g id="device_type" example="phone">%1$s</xliff:g></string>
+ <!-- Description of the privileges the application will get if associated with the companion device of GLASSES profile [CHAR LIMIT=NONE] -->
+ <string name="summary_glasses">This app will be allowed to access these permissions on your <xliff:g id="device_name" example="phone">%1$s</xliff:g></string>
<!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
@@ -97,9 +94,6 @@
<string name="profile_name_generic">device</string>
<!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
- <string name="summary_generic_single_device">This app will be able to sync info, like the name of someone calling, between your phone and <xliff:g id="device_name" example="My Watch">%1$s</xliff:g></string>
-
- <!-- Description of the privileges the application will get if associated with the companion device of unspecified profile (type) [CHAR LIMIT=NONE] -->
<string name="summary_generic">This app will be able to sync info, like the name of someone calling, between your phone and the chosen device</string>
<!-- ================= Buttons ================= -->
@@ -110,6 +104,9 @@
<!-- Negative button for the device-app association consent dialog [CHAR LIMIT=30] -->
<string name="consent_no">Don\u2019t allow</string>
+ <!-- Cancel button for the device chooser dialog [CHAR LIMIT=30] -->
+ <string name="consent_cancel">Cancel</string>
+
<!-- Back button for the helper consent dialog [CHAR LIMIT=30] -->
<string name="consent_back">Back</string>
diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml
index e85190b..222877b 100644
--- a/packages/CompanionDeviceManager/res/values/styles.xml
+++ b/packages/CompanionDeviceManager/res/values/styles.xml
@@ -69,11 +69,13 @@
<style name="PositiveButton"
parent="@android:style/Widget.Material.Button.Borderless.Colored">
- <item name="android:layout_width">300dp</item>
- <item name="android:layout_height">56dp</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginBottom">2dp</item>
<item name="android:textAllCaps">false</item>
<item name="android:textSize">14sp</item>
+ <item name="android:layout_marginStart">32dp</item>
+ <item name="android:layout_marginEnd">32dp</item>
<item name="android:textColor">@android:color/system_neutral1_900</item>
<item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
<item name="android:background">@drawable/btn_positive_bottom</item>
@@ -81,11 +83,13 @@
<style name="NegativeButton"
parent="@android:style/Widget.Material.Button.Borderless.Colored">
- <item name="android:layout_width">300dp</item>
- <item name="android:layout_height">56dp</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginTop">2dp</item>
<item name="android:textAllCaps">false</item>
<item name="android:textSize">14sp</item>
+ <item name="android:layout_marginStart">32dp</item>
+ <item name="android:layout_marginEnd">32dp</item>
<item name="android:textColor">@android:color/system_neutral1_900</item>
<item name="android:layout_marginTop">4dp</item>
<item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index 4154029..97016f5 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -27,10 +27,8 @@
import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState;
import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT;
-import static com.android.companiondevicemanager.CompanionDeviceResources.MULTI_DEVICES_SUMMARIES;
import static com.android.companiondevicemanager.CompanionDeviceResources.PERMISSION_TYPES;
import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME;
-import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILES_NAME_MULTI;
import static com.android.companiondevicemanager.CompanionDeviceResources.PROFILE_ICON;
import static com.android.companiondevicemanager.CompanionDeviceResources.SUMMARIES;
import static com.android.companiondevicemanager.CompanionDeviceResources.SUPPORTED_PROFILES;
@@ -121,6 +119,9 @@
private IAssociationRequestCallback mAppCallback;
private ResultReceiver mCdmServiceReceiver;
+ // Present for application's name.
+ private CharSequence mAppLabel;
+
// Always present widgets.
private TextView mTitle;
private TextView mSummary;
@@ -165,8 +166,7 @@
private @Nullable RecyclerView mDeviceListRecyclerView;
private @Nullable DeviceListAdapter mDeviceAdapter;
-
- // The recycler view is only shown for selfManaged and singleDevice association request.
+ // The recycler view is shown for non-null profile association request.
private @Nullable RecyclerView mPermissionListRecyclerView;
private @Nullable PermissionListAdapter mPermissionListAdapter;
@@ -178,8 +178,6 @@
// onActivityResult() after the association is created.
private @Nullable DeviceFilterPair<?> mSelectedDevice;
- private @Nullable List<Integer> mPermissionTypes;
-
private LinearLayoutManager mPermissionsLayoutManager = new LinearLayoutManager(this);
@Override
@@ -302,6 +300,8 @@
setContentView(R.layout.activity_confirmation);
+ mAppLabel = appLabel;
+
mConstraintList = findViewById(R.id.constraint_list);
mAssociationConfirmationDialog = findViewById(R.id.association_confirmation);
mVendorHeader = findViewById(R.id.vendor_header);
@@ -322,7 +322,6 @@
mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device);
mSingleDeviceSpinner = findViewById(R.id.spinner_single_device);
- mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick);
mPermissionListRecyclerView = findViewById(R.id.permission_list);
mPermissionListAdapter = new PermissionListAdapter(this);
@@ -468,8 +467,6 @@
throw new RuntimeException("Unsupported profile " + deviceProfile);
}
- mPermissionTypes = new ArrayList<>();
-
try {
vendorIcon = getVendorHeaderIcon(this, packageName, userId);
vendorName = getVendorHeaderName(this, packageName, userId);
@@ -486,17 +483,13 @@
}
title = getHtmlFromResources(this, TITLES.get(deviceProfile), deviceName);
- mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile));
+ setupPermissionList(deviceProfile);
// Summary is not needed for selfManaged dialog.
mSummary.setVisibility(View.GONE);
-
- setupPermissionList();
-
mTitle.setText(title);
mVendorHeaderName.setText(vendorName);
mVendorHeader.setVisibility(View.VISIBLE);
- mVendorHeader.setVisibility(View.VISIBLE);
mProfileIcon.setVisibility(View.GONE);
mDeviceListRecyclerView.setVisibility(View.GONE);
// Top and bottom borders should be gone for selfManaged dialog.
@@ -509,7 +502,9 @@
final String deviceProfile = mRequest.getDeviceProfile();
- mPermissionTypes = new ArrayList<>();
+ if (!SUPPORTED_PROFILES.contains(deviceProfile)) {
+ throw new RuntimeException("Unsupported profile " + deviceProfile);
+ }
CompanionDeviceDiscoveryService.getScanResult().observe(this,
deviceFilterPairs -> updateSingleDeviceUi(
@@ -529,75 +524,40 @@
if (deviceFilterPairs.isEmpty()) return;
mSelectedDevice = requireNonNull(deviceFilterPairs.get(0));
- // No need to show user consent dialog if it is a singleDevice
- // and isSkipPrompt(true) AssociationRequest.
- // See AssociationRequestsProcessor#mayAssociateWithoutPrompt.
- if (mRequest.isSkipPrompt()) {
- mSingleDeviceSpinner.setVisibility(View.GONE);
- onUserSelectedDevice(mSelectedDevice);
- return;
- }
- final String deviceName = mSelectedDevice.getDisplayName();
- final Spanned title;
- final Spanned summary;
- final Drawable profileIcon;
+ final Drawable profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
- if (!SUPPORTED_PROFILES.contains(deviceProfile)) {
- throw new RuntimeException("Unsupported profile " + deviceProfile);
- }
+ updatePermissionUi();
- if (deviceProfile == null) {
- summary = getHtmlFromResources(this, SUMMARIES.get(null), deviceName);
- mConstraintList.setVisibility(View.GONE);
- } else {
- summary = getHtmlFromResources(
- this, SUMMARIES.get(deviceProfile), getString(R.string.device_type));
- mPermissionTypes.addAll(PERMISSION_TYPES.get(deviceProfile));
- setupPermissionList();
- }
-
- title = getHtmlFromResources(this, TITLES.get(deviceProfile), appLabel, deviceName);
- profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
-
- mTitle.setText(title);
- mSummary.setText(summary);
mProfileIcon.setImageDrawable(profileIcon);
- mSingleDeviceSpinner.setVisibility(View.GONE);
mAssociationConfirmationDialog.setVisibility(View.VISIBLE);
+ mSingleDeviceSpinner.setVisibility(View.GONE);
}
private void initUiForMultipleDevices(CharSequence appLabel) {
if (DEBUG) Log.i(TAG, "initUiFor_MultipleDevices()");
- final String deviceProfile = mRequest.getDeviceProfile();
-
- final String profileName;
- final String profileNameMulti;
- final Spanned summary;
final Drawable profileIcon;
- final int summaryResourceId;
+ final Spanned title;
+ final String deviceProfile = mRequest.getDeviceProfile();
if (!SUPPORTED_PROFILES.contains(deviceProfile)) {
throw new RuntimeException("Unsupported profile " + deviceProfile);
}
- profileName = getString(PROFILES_NAME.get(deviceProfile));
- profileNameMulti = getString(PROFILES_NAME_MULTI.get(deviceProfile));
profileIcon = getIcon(this, PROFILE_ICON.get(deviceProfile));
- summaryResourceId = MULTI_DEVICES_SUMMARIES.get(deviceProfile);
if (deviceProfile == null) {
- summary = getHtmlFromResources(this, summaryResourceId);
+ title = getHtmlFromResources(this, R.string.chooser_title_non_profile, appLabel);
+ mButtonNotAllowMultipleDevices.setText(R.string.consent_no);
} else {
- summary = getHtmlFromResources(this, summaryResourceId, profileName, appLabel);
+ title = getHtmlFromResources(this,
+ R.string.chooser_title, getString(PROFILES_NAME.get(deviceProfile)));
}
- final Spanned title = getHtmlFromResources(
- this, R.string.chooser_title, profileNameMulti, appLabel);
+ mDeviceAdapter = new DeviceListAdapter(this, this::onDeviceClicked);
mTitle.setText(title);
- mSummary.setText(summary);
mProfileIcon.setImageDrawable(profileIcon);
mDeviceListRecyclerView.setAdapter(mDeviceAdapter);
@@ -613,6 +573,7 @@
mDeviceAdapter.setDevices(deviceFilterPairs);
});
+ mSummary.setVisibility(View.GONE);
// "Remove" consent button: users would need to click on the list item.
mButtonAllow.setVisibility(View.GONE);
mButtonNotAllow.setVisibility(View.GONE);
@@ -623,11 +584,9 @@
mMultipleDeviceSpinner.setVisibility(View.VISIBLE);
}
- private void onListItemClick(int position) {
- if (DEBUG) Log.d(TAG, "onListItemClick() " + position);
-
+ private void onDeviceClicked(int position) {
final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position);
-
+ // To prevent double tap on the selected device.
if (mSelectedDevice != null) {
if (DEBUG) Log.w(TAG, "Already selected.");
return;
@@ -637,7 +596,47 @@
mSelectedDevice = requireNonNull(selectedDevice);
- onUserSelectedDevice(selectedDevice);
+ Log.d(TAG, "onDeviceClicked(): " + mSelectedDevice.toShortString());
+
+ updatePermissionUi();
+
+ mSummary.setVisibility(View.VISIBLE);
+ mButtonAllow.setVisibility(View.VISIBLE);
+ mButtonNotAllow.setVisibility(View.VISIBLE);
+ mDeviceListRecyclerView.setVisibility(View.GONE);
+ mNotAllowMultipleDevicesLayout.setVisibility(View.GONE);
+ }
+
+ private void updatePermissionUi() {
+ final String deviceProfile = mRequest.getDeviceProfile();
+ final int summaryResourceId = SUMMARIES.get(deviceProfile);
+ final String remoteDeviceName = mSelectedDevice.getDisplayName();
+ final Spanned title = getHtmlFromResources(
+ this, TITLES.get(deviceProfile), mAppLabel, remoteDeviceName);
+ final Spanned summary;
+
+ // No need to show permission consent dialog if it is a isSkipPrompt(true)
+ // AssociationRequest. See AssociationRequestsProcessor#mayAssociateWithoutPrompt.
+ if (mRequest.isSkipPrompt()) {
+ mSingleDeviceSpinner.setVisibility(View.GONE);
+ onUserSelectedDevice(mSelectedDevice);
+ return;
+ }
+
+ if (deviceProfile == null && mRequest.isSingleDevice()) {
+ summary = getHtmlFromResources(this, summaryResourceId, remoteDeviceName);
+ mConstraintList.setVisibility(View.GONE);
+ } else if (deviceProfile == null) {
+ onUserSelectedDevice(mSelectedDevice);
+ return;
+ } else {
+ summary = getHtmlFromResources(
+ this, summaryResourceId, getString(R.string.device_type));
+ setupPermissionList(deviceProfile);
+ }
+
+ mTitle.setText(title);
+ mSummary.setText(summary);
}
private void onPositiveButtonClick(View v) {
@@ -680,8 +679,9 @@
// initiate the layoutManager for the recyclerview, add listeners for monitoring the scrolling
// and when mPermissionListRecyclerView is fully populated.
// Lastly, disable the Allow and Don't allow buttons.
- private void setupPermissionList() {
- mPermissionListAdapter.setPermissionType(mPermissionTypes);
+ private void setupPermissionList(String deviceProfile) {
+ final List<Integer> permissionTypes = new ArrayList<>(PERMISSION_TYPES.get(deviceProfile));
+ mPermissionListAdapter.setPermissionType(permissionTypes);
mPermissionListRecyclerView.setAdapter(mPermissionListAdapter);
mPermissionListRecyclerView.setLayoutManager(mPermissionsLayoutManager);
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
index 7aed139..060c032 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceResources.java
@@ -86,21 +86,11 @@
static final Map<String, Integer> SUMMARIES;
static {
final Map<String, Integer> map = new ArrayMap<>();
- map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch_single_device);
- map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_single_device);
- map.put(null, R.string.summary_generic_single_device);
-
- SUMMARIES = unmodifiableMap(map);
- }
-
- static final Map<String, Integer> MULTI_DEVICES_SUMMARIES;
- static {
- final Map<String, Integer> map = new ArrayMap<>();
map.put(DEVICE_PROFILE_WATCH, R.string.summary_watch);
- map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses_multi_device);
+ map.put(DEVICE_PROFILE_GLASSES, R.string.summary_glasses);
map.put(null, R.string.summary_generic);
- MULTI_DEVICES_SUMMARIES = unmodifiableMap(map);
+ SUMMARIES = unmodifiableMap(map);
}
static final Map<String, Integer> PROFILES_NAME;
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
index 2b38b4c..8e0cf89 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt
@@ -34,11 +34,11 @@
/**
* The repository to load the App List data.
*/
-internal interface AppListRepository {
+interface AppListRepository {
/** Loads the list of [ApplicationInfo]. */
suspend fun loadApps(
userId: Int,
- showInstantApps: Boolean = false,
+ loadInstantApps: Boolean = false,
matchAnyUserForAdmin: Boolean = false,
): List<ApplicationInfo>
@@ -50,6 +50,9 @@
/** Gets the system app package names. */
fun getSystemPackageNamesBlocking(userId: Int): Set<String>
+
+ /** Loads the list of [ApplicationInfo], and filter base on `isSystemApp`. */
+ suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean): List<ApplicationInfo>
}
/**
@@ -62,13 +65,13 @@
AppListRepositoryImpl(context).getSystemPackageNamesBlocking(userId)
}
-internal class AppListRepositoryImpl(private val context: Context) : AppListRepository {
+class AppListRepositoryImpl(private val context: Context) : AppListRepository {
private val packageManager = context.packageManager
private val userManager = context.userManager
override suspend fun loadApps(
userId: Int,
- showInstantApps: Boolean,
+ loadInstantApps: Boolean,
matchAnyUserForAdmin: Boolean,
): List<ApplicationInfo> = coroutineScope {
val hiddenSystemModulesDeferred = async {
@@ -86,7 +89,7 @@
val hiddenSystemModules = hiddenSystemModulesDeferred.await()
val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await()
installedApplicationsAsUser.filter { app ->
- app.isInAppList(showInstantApps, hiddenSystemModules, hideWhenDisabledPackages)
+ app.isInAppList(loadInstantApps, hiddenSystemModules, hideWhenDisabledPackages)
}
}
@@ -136,17 +139,17 @@
): Flow<(app: ApplicationInfo) -> Boolean> =
userIdFlow.combine(showSystemFlow, ::showSystemPredicate)
- override fun getSystemPackageNamesBlocking(userId: Int) =
- runBlocking { getSystemPackageNames(userId) }
+ override fun getSystemPackageNamesBlocking(userId: Int) = runBlocking {
+ loadAndFilterApps(userId = userId, isSystemApp = true).map { it.packageName }.toSet()
+ }
- private suspend fun getSystemPackageNames(userId: Int): Set<String> =
- coroutineScope {
- val loadAppsDeferred = async { loadApps(userId) }
- val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
- val showSystemPredicate =
- { app: ApplicationInfo -> isSystemApp(app, homeOrLauncherPackages) }
- loadAppsDeferred.await().filter(showSystemPredicate).map { it.packageName }.toSet()
+ override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) = coroutineScope {
+ val loadAppsDeferred = async { loadApps(userId) }
+ val homeOrLauncherPackages = loadHomeOrLauncherPackages(userId)
+ loadAppsDeferred.await().filter { app ->
+ isSystemApp(app, homeOrLauncherPackages) == isSystemApp
}
+ }
private suspend fun showSystemPredicate(
userId: Int,
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
index 302f780..375ed60 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt
@@ -108,7 +108,7 @@
val appList = repository.loadApps(
userId = ADMIN_USER_ID,
- showInstantApps = false,
+ loadInstantApps = false,
)
assertThat(appList).containsExactly(NORMAL_APP)
@@ -120,7 +120,7 @@
val appList = repository.loadApps(
userId = ADMIN_USER_ID,
- showInstantApps = true,
+ loadInstantApps = true,
)
assertThat(appList).containsExactly(NORMAL_APP, INSTANT_APP)
@@ -325,6 +325,21 @@
assertThat(systemPackageNames).containsExactly(SYSTEM_APP.packageName)
}
+ @Test
+ fun loadAndFilterApps_loadNonSystemApp_returnExpectedValues() = runTest {
+ mockInstalledApplications(
+ apps = listOf(
+ NORMAL_APP, INSTANT_APP, SYSTEM_APP, UPDATED_SYSTEM_APP, HOME_APP, IN_LAUNCHER_APP
+ ),
+ userId = ADMIN_USER_ID,
+ )
+
+ val appList = repository.loadAndFilterApps(userId = ADMIN_USER_ID, isSystemApp = false)
+
+ assertThat(appList)
+ .containsExactly(NORMAL_APP, UPDATED_SYSTEM_APP, HOME_APP, IN_LAUNCHER_APP)
+ }
+
private suspend fun getShowSystemPredicate(showSystem: Boolean) =
repository.showSystemPredicate(
userIdFlow = flowOf(ADMIN_USER_ID),
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
index 6889e5d..9b22497 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt
@@ -87,7 +87,7 @@
private object FakeAppListRepository : AppListRepository {
override suspend fun loadApps(
userId: Int,
- showInstantApps: Boolean,
+ loadInstantApps: Boolean,
matchAnyUserForAdmin: Boolean,
) = listOf(APP)
@@ -97,6 +97,9 @@
): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { true }
override fun getSystemPackageNamesBlocking(userId: Int): Set<String> = emptySet()
+
+ override suspend fun loadAndFilterApps(userId: Int, isSystemApp: Boolean) =
+ emptyList<ApplicationInfo>()
}
private object FakeAppRepository : AppRepository {
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 873b434..dd8eb3b 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -590,6 +590,7 @@
Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS,
Settings.Global.AUTO_REVOKE_PARAMETERS,
Settings.Global.ENABLE_RADIO_BUG_DETECTION,
+ Settings.Global.REPAIR_MODE_ACTIVE,
Settings.Global.RADIO_BUG_WAKELOCK_TIMEOUT_COUNT_THRESHOLD,
Settings.Global.RADIO_BUG_SYSTEM_ERROR_COUNT_THRESHOLD,
Settings.Global.ENABLED_SUBSCRIPTION_FOR_SLOT,
@@ -724,6 +725,7 @@
Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS,
Settings.Secure.CONTENT_CAPTURE_ENABLED,
Settings.Secure.DEFAULT_INPUT_METHOD,
+ Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
Settings.Secure.DEVICE_PAIRED,
Settings.Secure.DIALER_DEFAULT_APPLICATION,
Settings.Secure.DISABLED_PRINT_SERVICES,
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index fe90caf..476c820 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -176,6 +176,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT"/>
+ <uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<!-- Assist -->
<uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE" />
@@ -451,12 +452,14 @@
android:noHistory="true" />
<service android:name=".screenshot.appclips.AppClipsScreenshotHelperService"
- android:permission="com.android.systemui.permission.SELF"
- android:exported="false" />
+ android:exported="false"
+ android:singleUser="true"
+ android:permission="com.android.systemui.permission.SELF" />
<service android:name=".screenshot.appclips.AppClipsService"
- android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
- android:exported="true" />
+ android:exported="true"
+ android:singleUser="true"
+ android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE" />
<service android:name=".screenrecord.RecordingService"
android:foregroundServiceType="systemExempted"/>
@@ -990,6 +993,11 @@
<service android:name=".notetask.NoteTaskControllerUpdateService" />
+ <service android:name=".notetask.NoteTaskBubblesController$NoteTaskBubblesService"
+ android:exported="false"
+ android:singleUser="true"
+ android:permission="com.android.systemui.permission.SELF" />
+
<activity
android:name=".notetask.shortcut.LaunchNoteTaskActivity"
android:exported="true"
@@ -1003,16 +1011,6 @@
</intent-filter>
</activity>
- <!-- LaunchNoteTaskManagedProfileProxyActivity MUST NOT be exported because it allows caller
- to specify an Android user when launching the default notes app. -->
- <activity
- android:name=".notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity"
- android:exported="false"
- android:enabled="true"
- android:excludeFromRecents="true"
- android:resizeableActivity="false"
- android:theme="@android:style/Theme.NoDisplay" />
-
<activity
android:name=".notetask.LaunchNotesRoleSettingsTrampolineActivity"
android:exported="true"
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
index 8fe1f48..5d09de1 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
@@ -27,37 +27,37 @@
object Easings {
/** The standard interpolator that should be used on every normal animation */
- val StandardEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD)
+ val Standard = fromInterpolator(InterpolatorsAndroidX.STANDARD)
/**
* The standard accelerating interpolator that should be used on every regular movement of
* content that is disappearing e.g. when moving off screen.
*/
- val StandardAccelerateEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD_ACCELERATE)
+ val StandardAccelerate = fromInterpolator(InterpolatorsAndroidX.STANDARD_ACCELERATE)
/**
* The standard decelerating interpolator that should be used on every regular movement of
* content that is appearing e.g. when coming from off screen.
*/
- val StandardDecelerateEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD_DECELERATE)
+ val StandardDecelerate = fromInterpolator(InterpolatorsAndroidX.STANDARD_DECELERATE)
/** The default emphasized interpolator. Used for hero / emphasized movement of content. */
- val EmphasizedEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED)
+ val Emphasized = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED)
/**
* The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
* is disappearing e.g. when moving off screen.
*/
- val EmphasizedAccelerateEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_ACCELERATE)
+ val EmphasizedAccelerate = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_ACCELERATE)
/**
* The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
* is appearing e.g. when coming from off screen
*/
- val EmphasizedDecelerateEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_DECELERATE)
+ val EmphasizedDecelerate = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_DECELERATE)
/** The linear interpolator. */
- val LinearEasing = fromInterpolator(InterpolatorsAndroidX.LINEAR)
+ val Linear = fromInterpolator(InterpolatorsAndroidX.LINEAR)
private fun fromInterpolator(source: Interpolator) = Easing { x -> source.getInterpolation(x) }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 8844114..b3d2e35 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -131,7 +131,7 @@
animationSpec =
tween(
durationMillis = SELECTED_DOT_REACTION_ANIMATION_DURATION_MS,
- easing = Easings.StandardAccelerateEasing,
+ easing = Easings.StandardAccelerate,
),
)
} else {
@@ -140,7 +140,7 @@
animationSpec =
tween(
durationMillis = SELECTED_DOT_RETRACT_ANIMATION_DURATION_MS,
- easing = Easings.StandardDecelerateEasing,
+ easing = Easings.StandardDecelerate,
),
)
}
@@ -333,7 +333,7 @@
FAILURE_ANIMATION_DOT_SHRINK_ANIMATION_DURATION_MS,
delayMillis =
rowIndex * FAILURE_ANIMATION_DOT_SHRINK_STAGGER_DELAY_MS,
- easing = Easings.LinearEasing,
+ easing = Easings.Linear,
),
)
@@ -343,7 +343,7 @@
tween(
durationMillis =
FAILURE_ANIMATION_DOT_REVERT_ANIMATION_DURATION,
- easing = Easings.StandardEasing,
+ easing = Easings.Standard,
),
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 968e5ab..20b859a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -317,4 +317,4 @@
private val pinButtonPressedEasing = LinearEasing
private val pinButtonHoldTime = 33.milliseconds
private val pinButtonReleasedDuration = 420.milliseconds
-private val pinButtonReleasedEasing = Easings.StandardEasing
+private val pinButtonReleasedEasing = Easings.Standard
diff --git a/packages/SystemUI/res/layout/scene_window_root.xml b/packages/SystemUI/res/layout/scene_window_root.xml
new file mode 100644
index 0000000..0dcd15b
--- /dev/null
+++ b/packages/SystemUI/res/layout/scene_window_root.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2023, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<!-- The root view of the scene window. -->
+<com.android.systemui.scene.ui.view.SceneWindowRootView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:sysui="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/scene_window_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <include layout="@layout/super_notification_shade"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+</com.android.systemui.scene.ui.view.SceneWindowRootView>
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index d9fe949..01465d7 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -21,6 +21,7 @@
<com.android.systemui.shade.NotificationShadeWindowView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:sysui="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/legacy_window_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 67fdb4c..c619d46 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2390,6 +2390,8 @@
<string name="magnification_mode_switch_state_window">Magnify part of screen</string>
<!-- Click action label for magnification settings panel. [CHAR LIMIT=NONE] -->
<string name="magnification_open_settings_click_label">Open magnification settings</string>
+ <!-- Click action label for magnification settings panel. [CHAR LIMIT=NONE] -->
+ <string name="magnification_close_settings_click_label">Close magnification settings</string>
<!-- Label of the corner of a rectangle that you can tap and drag to resize the magnification area. [CHAR LIMIT=NONE] -->
<string name="magnification_drag_corner_to_resize">Drag corner to resize</string>
@@ -2971,9 +2973,11 @@
<xliff:g id="weather_condition" example="Partly cloudy">%1$s</xliff:g>, <xliff:g id="temperature" example="7°C">%2$s</xliff:g>
</string>
- <!-- TODO(b/259369672): Replace with final resource. -->
<!-- [CHAR LIMIT=30] Label used to open Note Task -->
- <string name="note_task_button_label">Notetaking</string>
+ <string name="note_task_button_label">Note-taking</string>
+
+ <!-- [CHAR LIMIT=25] Long label used by Note Task Shortcut -->
+ <string name="note_task_shortcut_long_label">Note-taking, <xliff:g id="note_taking_app" example="Note-taking App">%1$s</xliff:g></string>
<!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is broadcasting -->
<string name="broadcasting_description_is_broadcasting">Broadcasting</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 117cf78a..4b31498 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -48,7 +48,7 @@
*
* Normal gesture: DOWN, MOVE/POINTER_DOWN/POINTER_UP)*, UP or CANCLE
*/
- oneway void onStatusBarMotionEvent(in MotionEvent event) = 9;
+ oneway void onStatusBarTouchEvent(in MotionEvent event) = 9;
/**
* Proxies the assistant gesture's progress started from navigation bar.
@@ -125,5 +125,15 @@
*/
oneway void takeScreenshot(in ScreenshotRequest request) = 51;
- // Next id = 52
+ /**
+ * Dispatches trackpad status bar motion event to the notification shade. Currently these events
+ * are from the input monitor in {@link TouchInteractionService}. This is different from
+ * {@link #onStatusBarTouchEvent} above in that, this directly dispatches motion events to the
+ * notification shade, while {@link #onStatusBarTouchEvent} relies on setting the launcher
+ * window slippery to allow the frameworks to route those events after passing the initial
+ * threshold.
+ */
+ oneway void onStatusBarTrackpadEvent(in MotionEvent event) = 52;
+
+ // Next id = 53
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index e057188..7acfbf6 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -140,6 +140,7 @@
long elapsedRealtime = SystemClock.elapsedRealtime();
long secondsInFuture = (long) Math.ceil(
(elapsedRealtimeDeadline - elapsedRealtime) / 1000.0);
+ getKeyguardSecurityCallback().onAttemptLockoutStart(secondsInFuture);
mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) {
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index d9d64ad..a90a34c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -255,9 +255,10 @@
if (!animate) {
out.setAlpha(0f);
out.setTranslationY(clockOutYTranslation);
+ out.setVisibility(INVISIBLE);
in.setAlpha(1f);
in.setTranslationY(clockInYTranslation);
- in.setVisibility(View.VISIBLE);
+ in.setVisibility(VISIBLE);
mStatusArea.setTranslationY(statusAreaYTranslation);
return;
}
@@ -270,7 +271,10 @@
ObjectAnimator.ofFloat(out, TRANSLATION_Y, clockOutYTranslation));
mClockOutAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
- mClockOutAnim = null;
+ if (mClockOutAnim == animation) {
+ out.setVisibility(INVISIBLE);
+ mClockOutAnim = null;
+ }
}
});
@@ -285,7 +289,9 @@
mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS);
mClockInAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
- mClockInAnim = null;
+ if (mClockInAnim == animation) {
+ mClockInAnim = null;
+ }
}
});
@@ -299,7 +305,9 @@
ObjectAnimator.ofFloat(mSmallClockFrame, TRANSLATION_Y, statusAreaYTranslation));
mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
- mStatusAreaAnim = null;
+ if (mStatusAreaAnim == animation) {
+ mStatusAreaAnim = null;
+ }
}
});
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 99e2574..8f17edd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -431,10 +431,6 @@
int clockHeight = clock.getLargeClock().getView().getHeight();
return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2;
} else {
- // This is only called if we've never shown the large clock as the frame is inflated
- // with 'gone', but then the visibility is never set when it is animated away by
- // KeyguardClockSwitch, instead it is removed from the view hierarchy.
- // TODO(b/261755021): Cleanup Large Frame Visibility
int clockHeight = clock.getSmallClock().getView().getHeight();
return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin;
}
@@ -452,15 +448,11 @@
if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
return clock.getLargeClock().getView().getHeight();
} else {
- // Is not called except in certain edge cases, see comment in getClockBottom
- // TODO(b/261755021): Cleanup Large Frame Visibility
return clock.getSmallClock().getView().getHeight();
}
}
boolean isClockTopAligned() {
- // Returns false except certain edge cases, see comment in getClockBottom
- // TODO(b/261755021): Cleanup Large Frame Visibility
return mLargeClockFrame.getVisibility() != View.VISIBLE;
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 3c05299..b0a5d7c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -17,6 +17,7 @@
package com.android.keyguard;
import android.annotation.CallSuper;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.content.res.Resources;
@@ -33,6 +34,10 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor;
+import com.android.systemui.keyguard.bouncer.ui.BouncerMessageView;
+import com.android.systemui.keyguard.ui.binder.BouncerMessageViewBinder;
+import com.android.systemui.log.BouncerLogger;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.util.ViewController;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -168,6 +173,24 @@
/** Determines the message to show in the bouncer when it first appears. */
protected abstract int getInitialMessageResId();
+ /**
+ * Binds the {@link KeyguardInputView#getBouncerMessageView()} view with the provided context.
+ */
+ public void bindMessageView(
+ @NonNull BouncerMessageInteractor bouncerMessageInteractor,
+ KeyguardMessageAreaController.Factory messageAreaControllerFactory,
+ BouncerLogger bouncerLogger,
+ FeatureFlags featureFlags) {
+ BouncerMessageView bouncerMessageView = (BouncerMessageView) mView.getBouncerMessageView();
+ if (bouncerMessageView != null) {
+ BouncerMessageViewBinder.bind(bouncerMessageView,
+ bouncerMessageInteractor,
+ messageAreaControllerFactory,
+ bouncerLogger,
+ featureFlags);
+ }
+ }
+
/** Factory for a {@link KeyguardInputViewController}. */
public static class Factory {
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 64b1c50..bcf8e98 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -365,6 +365,7 @@
final long elapsedRealtime = SystemClock.elapsedRealtime();
final long secondsInFuture = (long) Math.ceil(
(elapsedRealtimeDeadline - elapsedRealtime) / 1000.0);
+ getKeyguardSecurityCallback().onAttemptLockoutStart(secondsInFuture);
mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) {
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
index bf9c3bb..2878df2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java
@@ -87,6 +87,11 @@
default void onUserInput() {
}
+ /**
+ * Invoked when the auth input is disabled for specified number of seconds.
+ * @param seconds Number of seconds for which the auth input is disabled.
+ */
+ default void onAttemptLockoutStart(long seconds) {}
/**
* Dismisses keyguard and go to unlocked state.
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index b5e5420..57df33e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -74,6 +74,7 @@
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.ActivityStarter;
@@ -116,6 +117,7 @@
private final Optional<SideFpsController> mSideFpsController;
private final FalsingA11yDelegate mFalsingA11yDelegate;
private final KeyguardFaceAuthInteractor mKeyguardFaceAuthInteractor;
+ private final BouncerMessageInteractor mBouncerMessageInteractor;
private int mTranslationY;
// Whether the volume keys should be handled by keyguard. If true, then
// they will be handled here for specific media types such as music, otherwise
@@ -178,6 +180,7 @@
@Override
public void onUserInput() {
+ mBouncerMessageInteractor.onPrimaryBouncerUserInput();
mKeyguardFaceAuthInteractor.onPrimaryBouncerUserInput();
mUpdateMonitor.cancelFaceAuth();
}
@@ -207,7 +210,15 @@
}
@Override
+ public void onAttemptLockoutStart(long seconds) {
+ mBouncerMessageInteractor.onPrimaryAuthLockedOut(seconds);
+ }
+
+ @Override
public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) {
+ if (timeoutMs == 0 && !success) {
+ mBouncerMessageInteractor.onPrimaryAuthIncorrectAttempt();
+ }
int bouncerSide = SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__DEFAULT;
if (mView.isSidedSecurityMode()) {
bouncerSide = mView.isSecurityLeftAligned()
@@ -392,7 +403,8 @@
TelephonyManager telephonyManager,
ViewMediatorCallback viewMediatorCallback,
AudioManager audioManager,
- KeyguardFaceAuthInteractor keyguardFaceAuthInteractor
+ KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
+ BouncerMessageInteractor bouncerMessageInteractor
) {
super(view);
mLockPatternUtils = lockPatternUtils;
@@ -418,6 +430,7 @@
mViewMediatorCallback = viewMediatorCallback;
mAudioManager = audioManager;
mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
+ mBouncerMessageInteractor = bouncerMessageInteractor;
}
@Override
@@ -438,6 +451,7 @@
// Update ViewMediator with the current input method requirements
mViewMediatorCallback.setNeedsInput(needsInput());
mView.setOnKeyListener(mOnKeyListener);
+
showPrimarySecurityScreen(false);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 94db31d..e57e85b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1462,14 +1462,6 @@
ErrorAuthenticationStatus error = (ErrorAuthenticationStatus) status;
handleFaceError(error.getMsgId(), error.getMsg());
} else if (status instanceof FailedAuthenticationStatus) {
- if (isFaceLockedOut()) {
- // TODO b/270090188: remove this hack when biometrics fixes this issue.
- // FailedAuthenticationStatus is emitted after ErrorAuthenticationStatus
- // for lockout error is received
- mLogger.d("onAuthenticationFailed called after"
- + " face has been locked out");
- return;
- }
handleFaceAuthFailed();
} else if (status instanceof HelpAuthenticationStatus) {
HelpAuthenticationStatus helpMsg = (HelpAuthenticationStatus) status;
@@ -1980,13 +1972,6 @@
@Override
public void onAuthenticationFailed() {
- if (isFaceLockedOut()) {
- // TODO b/270090188: remove this hack when biometrics fixes this issue.
- // onAuthenticationFailed is called after onAuthenticationError
- // for lockout error is received
- mLogger.d("onAuthenticationFailed called after face has been locked out");
- return;
- }
handleFaceAuthFailed();
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
index c3bb423..fd3c158 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationSettingsController.java
@@ -87,14 +87,15 @@
}
/**
- * Shows magnification settings panel {@link WindowMagnificationSettings}.
+ * Toggles the visibility of magnification settings panel {@link WindowMagnificationSettings}.
+ * We show the panel if it is not visible. Otherwise, hide the panel.
*/
- void showMagnificationSettings() {
+ void toggleSettingsPanelVisibility() {
if (!mWindowMagnificationSettings.isSettingPanelShowing()) {
onConfigurationChanged(mContext.getResources().getConfiguration());
mContext.registerComponentCallbacks(this);
}
- mWindowMagnificationSettings.showSettingPanel();
+ mWindowMagnificationSettings.toggleSettingsPanelVisibility();
}
void closeMagnificationSettings() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
index e2b85fa..2a14dc8 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java
@@ -171,7 +171,7 @@
mModeSwitchesController.setClickListenerDelegate(
displayId -> mHandler.post(() -> {
- showMagnificationSettingsPanel(displayId);
+ toggleSettingsPanelVisibility(displayId);
}));
}
@@ -254,11 +254,11 @@
}
@MainThread
- void showMagnificationSettingsPanel(int displayId) {
+ void toggleSettingsPanelVisibility(int displayId) {
final MagnificationSettingsController magnificationSettingsController =
mMagnificationSettingsSupplier.get(displayId);
if (magnificationSettingsController != null) {
- magnificationSettingsController.showMagnificationSettings();
+ magnificationSettingsController.toggleSettingsPanelVisibility();
}
}
@@ -335,7 +335,7 @@
@Override
public void onClickSettingsButton(int displayId) {
mHandler.post(() -> {
- showMagnificationSettingsPanel(displayId);
+ toggleSettingsPanelVisibility(displayId);
});
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index a67f706..7c76588 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -221,6 +221,7 @@
private boolean mAllowDiagonalScrolling = false;
private boolean mEditSizeEnable = false;
+ private boolean mSettingsPanelVisibility = false;
@Nullable
private final MirrorWindowControl mMirrorWindowControl;
@@ -1399,6 +1400,8 @@
return;
}
+ mSettingsPanelVisibility = settingsPanelIsShown;
+
mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown
? R.drawable.accessibility_window_magnification_drag_handle_background_change
: R.drawable.accessibility_window_magnification_drag_handle_background));
@@ -1439,12 +1442,19 @@
private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate {
+ private CharSequence getClickAccessibilityActionLabel() {
+ return mSettingsPanelVisibility
+ ? mContext.getResources().getString(
+ R.string.magnification_close_settings_click_label)
+ : mContext.getResources().getString(
+ R.string.magnification_open_settings_click_label);
+ }
+
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
final AccessibilityAction clickAction = new AccessibilityAction(
- AccessibilityAction.ACTION_CLICK.getId(), mContext.getResources().getString(
- R.string.magnification_open_settings_click_label));
+ AccessibilityAction.ACTION_CLICK.getId(), getClickAccessibilityActionLabel());
info.addAction(clickAction);
info.setClickable(true);
info.addAction(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 3b1d695..d27087d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -79,7 +79,8 @@
private final Runnable mWindowInsetChangeRunnable;
private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
- private final LayoutParams mParams;
+ @VisibleForTesting
+ final LayoutParams mParams;
@VisibleForTesting
final Rect mDraggableWindowBounds = new Rect();
private boolean mIsVisible = false;
@@ -311,6 +312,14 @@
mCallback.onSettingsPanelVisibilityChanged(/* shown= */ false);
}
+ public void toggleSettingsPanelVisibility() {
+ if (!mIsVisible) {
+ showSettingPanel();
+ } else {
+ hideSettingPanel();
+ }
+ }
+
public void showSettingPanel() {
showSettingPanel(true);
}
@@ -521,7 +530,6 @@
// CONFIG_FONT_SCALE: font size change
// CONFIG_LOCALE: language change
// CONFIG_DENSITY: display size change
-
mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext);
boolean showSettingPanelAfterConfigChange = mIsVisible;
@@ -533,16 +541,13 @@
return;
}
- if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
- final Rect previousDraggableBounds = new Rect(mDraggableWindowBounds);
+ if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0
+ || (configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
mDraggableWindowBounds.set(getDraggableWindowBounds());
- // Keep the Y position with the same height ratio before the window bounds and
- // draggable bounds are changed.
- final float windowHeightFraction = (float) (mParams.y - previousDraggableBounds.top)
- / previousDraggableBounds.height();
- mParams.y = (int) (windowHeightFraction * mDraggableWindowBounds.height())
- + mDraggableWindowBounds.top;
- return;
+ // reset the panel position to the right-bottom corner
+ mParams.x = mDraggableWindowBounds.right;
+ mParams.y = mDraggableWindowBounds.bottom;
+ updateButtonViewLayoutIfNeeded();
}
}
@@ -554,7 +559,8 @@
mDraggableWindowBounds.set(newBounds);
}
- private void updateButtonViewLayoutIfNeeded() {
+ @VisibleForTesting
+ void updateButtonViewLayoutIfNeeded() {
if (mIsVisible) {
mParams.x = MathUtils.constrain(mParams.x, mDraggableWindowBounds.left,
mDraggableWindowBounds.right);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 498e597..870fff7 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -72,10 +72,6 @@
val NOTIFICATION_MEMORY_LOGGING_ENABLED =
unreleasedFlag(119, "notification_memory_logging_enabled")
- @JvmField
- val SIMPLIFIED_APPEAR_FRACTION =
- releasedFlag(259395680, "simplified_appear_fraction")
-
// TODO(b/257315550): Tracking Bug
val NO_HUN_FOR_OLD_WHEN = releasedFlag(118, "no_hun_for_old_when")
@@ -359,7 +355,8 @@
// TODO(b/280426085): Tracking Bug
@JvmField
- val NEW_BLUETOOTH_REPOSITORY = unreleasedFlag(612, "new_bluetooth_repository")
+ val NEW_BLUETOOTH_REPOSITORY =
+ unreleasedFlag(612, "new_bluetooth_repository", teamfood = true)
// 700 - dialer/calls
// TODO(b/254512734): Tracking Bug
@@ -573,7 +570,7 @@
// TODO(b/270987164): Tracking Bug
@JvmField
- val TRACKPAD_GESTURE_FEATURES = unreleasedFlag(1205, "trackpad_gesture_features", teamfood = true)
+ val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
// TODO(b/263826204): Tracking Bug
@JvmField
@@ -632,6 +629,9 @@
// TODO(b/265944639): Tracking Bug
@JvmField val DUAL_SHADE = unreleasedFlag(1801, "dual_shade")
+ // TODO(b/283300105): Tracking Bug
+ @JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container")
+
// 1900
@JvmField val NOTE_TASKS = releasedFlag(1900, "keycode_flag")
@@ -706,8 +706,7 @@
// TODO(b/259428678): Tracking Bug
@JvmField
- val KEYBOARD_BACKLIGHT_INDICATOR =
- unreleasedFlag(2601, "keyboard_backlight_indicator", teamfood = true)
+ val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
// TODO(b/277192623): Tracking Bug
@JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardLifecyclesDispatcher.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardLifecyclesDispatcher.java
index 757afb6..67c85bd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardLifecyclesDispatcher.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardLifecyclesDispatcher.java
@@ -16,11 +16,19 @@
package com.android.systemui.keyguard;
+import android.annotation.IntDef;
import android.os.Handler;
+import android.os.Looper;
import android.os.Message;
-import android.os.Trace;
+import android.os.TraceNameSupplier;
+
+import androidx.annotation.NonNull;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import javax.inject.Inject;
@@ -29,7 +37,6 @@
*/
@SysUISingleton
public class KeyguardLifecyclesDispatcher {
-
static final int SCREEN_TURNING_ON = 0;
static final int SCREEN_TURNED_ON = 1;
static final int SCREEN_TURNING_OFF = 2;
@@ -39,19 +46,46 @@
static final int FINISHED_WAKING_UP = 5;
static final int STARTED_GOING_TO_SLEEP = 6;
static final int FINISHED_GOING_TO_SLEEP = 7;
- private static final String TAG = "KeyguardLifecyclesDispatcher";
- private final ScreenLifecycle mScreenLifecycle;
- private final WakefulnessLifecycle mWakefulnessLifecycle;
-
- @Inject
- public KeyguardLifecyclesDispatcher(ScreenLifecycle screenLifecycle,
- WakefulnessLifecycle wakefulnessLifecycle) {
- mScreenLifecycle = screenLifecycle;
- mWakefulnessLifecycle = wakefulnessLifecycle;
+ @IntDef({
+ SCREEN_TURNING_ON,
+ SCREEN_TURNED_ON,
+ SCREEN_TURNING_OFF,
+ SCREEN_TURNED_OFF,
+ STARTED_WAKING_UP,
+ FINISHED_WAKING_UP,
+ STARTED_GOING_TO_SLEEP,
+ FINISHED_GOING_TO_SLEEP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface KeyguardLifecycleMessageType {
}
- void dispatch(int what) {
+ private static String getNameOfMessage(@KeyguardLifecycleMessageType int what) {
+ return switch (what) {
+ case SCREEN_TURNING_ON -> "SCREEN_TURNING_ON";
+ case SCREEN_TURNED_ON -> "SCREEN_TURNED_ON";
+ case SCREEN_TURNING_OFF -> "SCREEN_TURNING_OFF";
+ case SCREEN_TURNED_OFF -> "SCREEN_TURNED_OFF";
+ case STARTED_WAKING_UP -> "STARTED_WAKING_UP";
+ case FINISHED_WAKING_UP -> "FINISHED_WAKING_UP";
+ case STARTED_GOING_TO_SLEEP -> "STARTED_GOING_TO_SLEEP";
+ case FINISHED_GOING_TO_SLEEP -> "FINISHED_GOING_TO_SLEEP";
+ default -> "UNKNOWN";
+ };
+ }
+
+ private final Handler mHandler;
+
+ @Inject
+ public KeyguardLifecyclesDispatcher(
+ @Main Looper mainLooper,
+ ScreenLifecycle screenLifecycle,
+ WakefulnessLifecycle wakefulnessLifecycle) {
+ mHandler = new KeyguardLifecycleHandler(mainLooper, screenLifecycle, wakefulnessLifecycle);
+ }
+
+ void dispatch(@KeyguardLifecycleMessageType int what) {
mHandler.obtainMessage(what).sendToTarget();
}
@@ -60,7 +94,7 @@
* @param pmReason Reason this message was triggered - this should be a value from either
* {@link PowerManager.WakeReason} or {@link PowerManager.GoToSleepReason}.
*/
- void dispatch(int what, int pmReason) {
+ void dispatch(@KeyguardLifecycleMessageType int what, int pmReason) {
final Message message = mHandler.obtainMessage(what);
message.arg1 = pmReason;
message.sendToTarget();
@@ -70,44 +104,48 @@
* @param what Message to send.
* @param object Object to send with the message
*/
- void dispatch(int what, Object object) {
+ void dispatch(@KeyguardLifecycleMessageType int what, Object object) {
mHandler.obtainMessage(what, object).sendToTarget();
}
- private Handler mHandler = new Handler() {
+ private static class KeyguardLifecycleHandler extends Handler {
+ private static final String TAG = "KeyguardLifecycleHandler";
+ private final ScreenLifecycle mScreenLifecycle;
+ private final WakefulnessLifecycle mWakefulnessLifecycle;
+
+ public KeyguardLifecycleHandler(Looper looper,
+ ScreenLifecycle screenLifecycle,
+ WakefulnessLifecycle wakefulnessLifecycle) {
+ super(looper);
+ mScreenLifecycle = screenLifecycle;
+ mWakefulnessLifecycle = wakefulnessLifecycle;
+ }
+
+ @NonNull
@Override
- public void handleMessage(Message msg) {
+ public String getTraceName(@NonNull Message msg) {
+ if (msg.getCallback() instanceof TraceNameSupplier || msg.getCallback() != null) {
+ return super.getTraceName(msg);
+ }
+ return TAG + "#" + getNameOfMessage(msg.what);
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
- case SCREEN_TURNING_ON:
- Trace.beginSection("KeyguardLifecyclesDispatcher#SCREEN_TURNING_ON");
- mScreenLifecycle.dispatchScreenTurningOn();
- Trace.endSection();
- break;
- case SCREEN_TURNED_ON:
- mScreenLifecycle.dispatchScreenTurnedOn();
- break;
- case SCREEN_TURNING_OFF:
- mScreenLifecycle.dispatchScreenTurningOff();
- break;
- case SCREEN_TURNED_OFF:
- mScreenLifecycle.dispatchScreenTurnedOff();
- break;
- case STARTED_WAKING_UP:
- mWakefulnessLifecycle.dispatchStartedWakingUp(msg.arg1 /* pmReason */);
- break;
- case FINISHED_WAKING_UP:
- mWakefulnessLifecycle.dispatchFinishedWakingUp();
- break;
- case STARTED_GOING_TO_SLEEP:
- mWakefulnessLifecycle.dispatchStartedGoingToSleep(msg.arg1 /* pmReason */);
- break;
- case FINISHED_GOING_TO_SLEEP:
- mWakefulnessLifecycle.dispatchFinishedGoingToSleep();
- break;
- default:
- throw new IllegalArgumentException("Unknown message: " + msg);
+ case SCREEN_TURNING_ON -> mScreenLifecycle.dispatchScreenTurningOn();
+ case SCREEN_TURNED_ON -> mScreenLifecycle.dispatchScreenTurnedOn();
+ case SCREEN_TURNING_OFF -> mScreenLifecycle.dispatchScreenTurningOff();
+ case SCREEN_TURNED_OFF -> mScreenLifecycle.dispatchScreenTurnedOff();
+ case STARTED_WAKING_UP ->
+ mWakefulnessLifecycle.dispatchStartedWakingUp(msg.arg1 /* pmReason */);
+ case FINISHED_WAKING_UP -> mWakefulnessLifecycle.dispatchFinishedWakingUp();
+ case STARTED_GOING_TO_SLEEP ->
+ mWakefulnessLifecycle.dispatchStartedGoingToSleep(msg.arg1 /* pmReason */);
+ case FINISHED_GOING_TO_SLEEP ->
+ mWakefulnessLifecycle.dispatchFinishedGoingToSleep();
+ default -> throw new IllegalArgumentException("Unknown message: " + msg);
}
}
- };
-
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 45e4623..81f325f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -17,6 +17,8 @@
package com.android.systemui.keyguard;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
+import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT;
+import static android.provider.Settings.System.LOCKSCREEN_SOUNDS_ENABLED;
import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT;
import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_NO_WINDOW_ANIMATIONS;
import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT;
@@ -70,7 +72,6 @@
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
@@ -93,7 +94,6 @@
import android.view.WindowManagerPolicyConstants;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
-import android.window.IRemoteTransition;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -154,6 +154,9 @@
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.settings.SystemSettings;
+import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.keyguard.KeyguardTransitions;
import dagger.Lazy;
@@ -161,7 +164,6 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Optional;
import java.util.concurrent.Executor;
/**
@@ -215,7 +217,7 @@
private final static String TAG = "KeyguardViewMediator";
- private static final String DELAYED_KEYGUARD_ACTION =
+ public static final String DELAYED_KEYGUARD_ACTION =
"com.android.internal.policy.impl.PhoneWindowManager.DELAYED_KEYGUARD";
private static final String DELAYED_LOCK_PROFILE_ACTION =
"com.android.internal.policy.impl.PhoneWindowManager.DELAYED_LOCK";
@@ -250,7 +252,7 @@
* turning on the keyguard (i.e, the user has this much time to turn
* the screen back on without having to face the keyguard).
*/
- private static final int KEYGUARD_LOCK_AFTER_DELAY_DEFAULT = 5000;
+ public static final int KEYGUARD_LOCK_AFTER_DELAY_DEFAULT = 5000;
/**
* How long we'll wait for the {@link ViewMediatorCallback#keyguardDoneDrawing()}
@@ -306,6 +308,9 @@
/** UserSwitcherController for creating guest user on boot complete */
private final UserSwitcherController mUserSwitcherController;
+ private final SecureSettings mSecureSettings;
+ private final SystemSettings mSystemSettings;
+ private final SystemClock mSystemClock;
/**
* Used to keep the device awake while to ensure the keyguard finishes opening before
@@ -1253,7 +1258,10 @@
Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy,
Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
Lazy<ScrimController> scrimControllerLazy,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ SecureSettings secureSettings,
+ SystemSettings systemSettings,
+ SystemClock systemClock) {
mContext = context;
mUserTracker = userTracker;
mFalsingCollector = falsingCollector;
@@ -1267,6 +1275,9 @@
mPM = powerManager;
mTrustManager = trustManager;
mUserSwitcherController = userSwitcherController;
+ mSecureSettings = secureSettings;
+ mSystemSettings = systemSettings;
+ mSystemClock = systemClock;
mStatusBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
mKeyguardDisplayManager = keyguardDisplayManager;
@@ -1316,7 +1327,7 @@
}
public void userActivity() {
- mPM.userActivity(SystemClock.uptimeMillis(), false);
+ mPM.userActivity(mSystemClock.uptimeMillis(), false);
}
private void setupLocked() {
@@ -1502,7 +1513,7 @@
if (cameraGestureTriggered) {
// Just to make sure, make sure the device is awake.
- mContext.getSystemService(PowerManager.class).wakeUp(SystemClock.uptimeMillis(),
+ mContext.getSystemService(PowerManager.class).wakeUp(mSystemClock.uptimeMillis(),
PowerManager.WAKE_REASON_CAMERA_LAUNCH,
"com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK");
setPendingLock(false);
@@ -1599,12 +1610,11 @@
// to enable it a bit later (i.e, give the user a chance
// to turn the screen back on within a certain window without
// having to unlock the screen)
- final ContentResolver cr = mContext.getContentResolver();
// From SecuritySettings
- final long lockAfterTimeout = Settings.Secure.getInt(cr,
- Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
- KEYGUARD_LOCK_AFTER_DELAY_DEFAULT);
+ final long lockAfterTimeout = mSecureSettings.getIntForUser(LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+ KEYGUARD_LOCK_AFTER_DELAY_DEFAULT,
+ userId);
// From DevicePolicyAdmin
final long policyTimeout = mLockPatternUtils.getDevicePolicyManager()
@@ -1616,8 +1626,9 @@
timeout = lockAfterTimeout;
} else {
// From DisplaySettings
- long displayTimeout = Settings.System.getInt(cr, SCREEN_OFF_TIMEOUT,
- KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT);
+ long displayTimeout = mSystemSettings.getIntForUser(SCREEN_OFF_TIMEOUT,
+ KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT,
+ userId);
// policy in effect. Make sure we don't go beyond policy limit.
displayTimeout = Math.max(displayTimeout, 0); // ignore negative values
@@ -1638,7 +1649,7 @@
private void doKeyguardLaterLocked(long timeout) {
// Lock in the future
- long when = SystemClock.elapsedRealtime() + timeout;
+ long when = mSystemClock.elapsedRealtime() + timeout;
Intent intent = new Intent(DELAYED_KEYGUARD_ACTION);
intent.setPackage(mContext.getPackageName());
intent.putExtra("seq", mDelayedShowingSequence);
@@ -1660,7 +1671,7 @@
if (userTimeout == 0) {
doKeyguardForChildProfilesLocked();
} else {
- long userWhen = SystemClock.elapsedRealtime() + userTimeout;
+ long userWhen = mSystemClock.elapsedRealtime() + userTimeout;
Intent lockIntent = new Intent(DELAYED_LOCK_PROFILE_ACTION);
lockIntent.setPackage(mContext.getPackageName());
lockIntent.putExtra("seq", mDelayedProfileShowingSequence);
@@ -2468,8 +2479,9 @@
private void playSound(int soundId) {
if (soundId == 0) return;
- final ContentResolver cr = mContext.getContentResolver();
- if (Settings.System.getInt(cr, Settings.System.LOCKSCREEN_SOUNDS_ENABLED, 1) == 1) {
+ int lockscreenSoundsEnabled = mSystemSettings.getIntForUser(LOCKSCREEN_SOUNDS_ENABLED, 1,
+ KeyguardUpdateMonitor.getCurrentUser());
+ if (lockscreenSoundsEnabled == 1) {
mLockSounds.stop(mLockSoundStreamId);
// Init mAudioManager
@@ -2644,7 +2656,7 @@
// It's possible that the device was unlocked (via BOUNCER) while dozing. It's time to
// wake up.
if (mAodShowing) {
- mPM.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ mPM.wakeUp(mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
"com.android.systemui:BOUNCER_DOZING");
}
@@ -2659,7 +2671,7 @@
// TODO(bc-unlock): Fill parameters
mNotificationShadeWindowControllerLazy.get().batchApplyWindowLayoutParams(() -> {
handleStartKeyguardExitAnimation(
- SystemClock.uptimeMillis() + mHideAnimation.getStartOffset(),
+ mSystemClock.uptimeMillis() + mHideAnimation.getStartOffset(),
mHideAnimation.getDuration(), null /* apps */, null /* wallpapers */,
null /* nonApps */, null /* finishedCallback */);
});
@@ -2668,7 +2680,7 @@
// It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
// dreaming. It's time to wake up.
if (mDreamOverlayShowing) {
- mPM.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ mPM.wakeUp(mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
"com.android.systemui:UNLOCK_DREAMING");
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index d7c039d..1c5bb5f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -64,6 +64,9 @@
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.settings.SystemSettings;
+import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.keyguard.KeyguardTransitions;
import dagger.Lazy;
@@ -128,7 +131,10 @@
Lazy<NotificationShadeWindowController> notificationShadeWindowController,
Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
Lazy<ScrimController> scrimControllerLazy,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ SecureSettings secureSettings,
+ SystemSettings systemSettings,
+ SystemClock systemClock) {
return new KeyguardViewMediator(
context,
uiEventLogger,
@@ -162,7 +168,10 @@
notificationShadeWindowController,
activityLaunchAnimator,
scrimControllerLazy,
- featureFlags);
+ featureFlags,
+ secureSettings,
+ systemSettings,
+ systemClock);
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/BouncerMessageViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/BouncerMessageViewBinder.kt
new file mode 100644
index 0000000..5b40dd7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/BouncerMessageViewBinder.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import android.text.TextUtils
+import android.util.PluralsMessageFormatter
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.BouncerKeyguardMessageArea
+import com.android.keyguard.KeyguardMessageArea
+import com.android.keyguard.KeyguardMessageAreaController
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.keyguard.bouncer.shared.model.Message
+import com.android.systemui.keyguard.bouncer.ui.BouncerMessageView
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.BouncerLogger
+import kotlinx.coroutines.launch
+
+object BouncerMessageViewBinder {
+ @JvmStatic
+ fun bind(
+ view: BouncerMessageView,
+ interactor: BouncerMessageInteractor,
+ factory: KeyguardMessageAreaController.Factory,
+ bouncerLogger: BouncerLogger,
+ featureFlags: FeatureFlags
+ ) {
+ view.repeatWhenAttached {
+ if (!featureFlags.isEnabled(Flags.REVAMPED_BOUNCER_MESSAGES)) {
+ view.primaryMessageView?.disable()
+ view.secondaryMessageView?.disable()
+ return@repeatWhenAttached
+ }
+ view.init(factory)
+ view.primaryMessage?.setIsVisible(true)
+ view.secondaryMessage?.setIsVisible(true)
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ bouncerLogger.startBouncerMessageInteractor()
+ launch {
+ interactor.bouncerMessage.collect {
+ bouncerLogger.bouncerMessageUpdated(it)
+ updateView(
+ view.primaryMessage,
+ view.primaryMessageView,
+ message = it?.message,
+ allowTruncation = true,
+ )
+ updateView(
+ view.secondaryMessage,
+ view.secondaryMessageView,
+ message = it?.secondaryMessage,
+ allowTruncation = false,
+ )
+ view.requestLayout()
+ }
+ }
+ }
+ }
+ }
+
+ private fun updateView(
+ controller: KeyguardMessageAreaController<KeyguardMessageArea>?,
+ view: BouncerKeyguardMessageArea?,
+ message: Message?,
+ allowTruncation: Boolean = false,
+ ) {
+ if (view == null || controller == null) return
+ if (message?.message != null || message?.messageResId != null) {
+ controller.setIsVisible(true)
+ var newMessage = message.message ?: ""
+ if (message.messageResId != null && message.messageResId != 0) {
+ newMessage = view.resources.getString(message.messageResId)
+ if (message.formatterArgs != null) {
+ newMessage =
+ PluralsMessageFormatter.format(
+ view.resources,
+ message.formatterArgs,
+ message.messageResId
+ )
+ }
+ }
+ controller.setMessage(newMessage, message.animate)
+ } else {
+ controller.setIsVisible(false)
+ controller.setMessage(0)
+ }
+ message?.colorState?.let { controller.setNextMessageColor(it) }
+ view.ellipsize =
+ if (allowTruncation) TextUtils.TruncateAt.END else TextUtils.TruncateAt.MARQUEE
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index c1aefc7..dd1a9d2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -22,16 +22,20 @@
import android.window.OnBackAnimationCallback
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.KeyguardMessageAreaController
import com.android.keyguard.KeyguardSecurityContainerController
import com.android.keyguard.KeyguardSecurityModel
import com.android.keyguard.KeyguardSecurityView
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.keyguard.data.BouncerViewDelegate
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.BouncerLogger
import com.android.systemui.plugins.ActivityStarter
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.filter
@@ -44,7 +48,11 @@
view: ViewGroup,
viewModel: KeyguardBouncerViewModel,
primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
- componentFactory: KeyguardBouncerComponent.Factory
+ componentFactory: KeyguardBouncerComponent.Factory,
+ messageAreaControllerFactory: KeyguardMessageAreaController.Factory,
+ bouncerMessageInteractor: BouncerMessageInteractor,
+ bouncerLogger: BouncerLogger,
+ featureFlags: FeatureFlags,
) {
// Builds the KeyguardSecurityContainerController from bouncer view group.
val securityContainerController: KeyguardSecurityContainerController =
@@ -125,8 +133,16 @@
securityContainerController.onResume(
KeyguardSecurityView.SCREEN_ON
)
+ bouncerLogger.bindingBouncerMessageView()
+ it.bindMessageView(
+ bouncerMessageInteractor,
+ messageAreaControllerFactory,
+ bouncerLogger,
+ featureFlags
+ )
}
} else {
+ bouncerMessageInteractor.onBouncerBeingHidden()
securityContainerController.onBouncerVisibilityChanged(
/* isVisible= */ false
)
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index e524189..cfb581b 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -22,6 +22,7 @@
import static com.android.systemui.classifier.Classifier.BACK_GESTURE;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadFourFingerSwipe;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadMultiFingerSwipe;
+import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -39,6 +40,7 @@
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.icu.text.SimpleDateFormat;
+import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -197,6 +199,7 @@
private final ViewConfiguration mViewConfiguration;
private final WindowManager mWindowManager;
private final IWindowManager mWindowManagerService;
+ private final InputManager mInputManager;
private final Optional<Pip> mPipOptional;
private final Optional<DesktopMode> mDesktopModeOptional;
private final FalsingManager mFalsingManager;
@@ -208,6 +211,7 @@
private final int mDisplayId;
private final Executor mMainExecutor;
+ private final Handler mMainHandler;
private final Executor mBackgroundExecutor;
private final Rect mPipExcludedBounds = new Rect();
@@ -252,6 +256,8 @@
private boolean mIsAttached;
private boolean mIsGesturalModeEnabled;
+ private boolean mIsTrackpadConnected;
+ private boolean mUsingThreeButtonNav;
private boolean mIsEnabled;
private boolean mIsNavBarShownTransiently;
private boolean mIsBackGestureAllowed;
@@ -351,12 +357,48 @@
}
};
+ private final InputManager.InputDeviceListener mInputDeviceListener =
+ new InputManager.InputDeviceListener() {
+
+ // Only one trackpad can be connected to a device at a time, since it takes over the
+ // only USB port.
+ private int mTrackpadDeviceId;
+
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ if (isTrackpadDevice(deviceId)) {
+ mTrackpadDeviceId = deviceId;
+ update(true /* isTrackpadConnected */);
+ }
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) { }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ if (mTrackpadDeviceId == deviceId) {
+ update(false /* isTrackpadConnected */);
+ }
+ }
+
+ private void update(boolean isTrackpadConnected) {
+ boolean isPreviouslyTrackpadConnected = mIsTrackpadConnected;
+ mIsTrackpadConnected = isTrackpadConnected;
+ if (isPreviouslyTrackpadConnected != mIsTrackpadConnected) {
+ updateIsEnabled();
+ updateCurrentUserResources();
+ }
+ }
+ };
+
EdgeBackGestureHandler(
Context context,
OverviewProxyService overviewProxyService,
SysUiState sysUiState,
PluginManager pluginManager,
@Main Executor executor,
+ @Main Handler handler,
@Background Executor backgroundExecutor,
UserTracker userTracker,
ProtoTracer protoTracer,
@@ -365,6 +407,7 @@
ViewConfiguration viewConfiguration,
WindowManager windowManager,
IWindowManager windowManagerService,
+ InputManager inputManager,
Optional<Pip> pipOptional,
Optional<DesktopMode> desktopModeOptional,
FalsingManager falsingManager,
@@ -375,6 +418,7 @@
mContext = context;
mDisplayId = context.getDisplayId();
mMainExecutor = executor;
+ mMainHandler = handler;
mBackgroundExecutor = backgroundExecutor;
mUserTracker = userTracker;
mOverviewProxyService = overviewProxyService;
@@ -386,6 +430,7 @@
mViewConfiguration = viewConfiguration;
mWindowManager = windowManager;
mWindowManagerService = windowManagerService;
+ mInputManager = inputManager;
mPipOptional = pipOptional;
mDesktopModeOptional = desktopModeOptional;
mFalsingManager = falsingManager;
@@ -394,6 +439,8 @@
mFeatureFlags = featureFlags;
mLightBarControllerProvider = lightBarControllerProvider;
mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
+ mIsTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(
+ Flags.TRACKPAD_GESTURE_FEATURES);
ComponentName recentsComponentName = ComponentName.unflattenFromString(
context.getString(com.android.internal.R.string.config_recentsComponentName));
if (recentsComponentName != null) {
@@ -425,7 +472,7 @@
ViewConfiguration.getLongPressTimeout());
mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
- mContext.getMainThreadHandler(), mContext, this::onNavigationSettingsChanged);
+ mMainHandler, mContext, this::onNavigationSettingsChanged);
updateCurrentUserResources();
}
@@ -512,6 +559,15 @@
mProtoTracer.add(this);
mOverviewProxyService.addCallback(mQuickSwitchListener);
mSysUiState.addCallback(mSysUiStateCallback);
+ if (mIsTrackpadGestureFeaturesEnabled) {
+ mInputManager.registerInputDeviceListener(mInputDeviceListener, mMainHandler);
+ int [] inputDevices = mInputManager.getInputDeviceIds();
+ for (int inputDeviceId : inputDevices) {
+ if (isTrackpadDevice(inputDeviceId)) {
+ mIsTrackpadConnected = true;
+ }
+ }
+ }
updateIsEnabled();
mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
}
@@ -524,6 +580,7 @@
mProtoTracer.remove(this);
mOverviewProxyService.removeCallback(mQuickSwitchListener);
mSysUiState.removeCallback(mSysUiStateCallback);
+ mInputManager.unregisterInputDeviceListener(mInputDeviceListener);
updateIsEnabled();
mUserTracker.removeCallback(mUserChangedCallback);
}
@@ -532,7 +589,9 @@
* @see NavigationModeController.ModeChangedListener#onNavigationModeChanged
*/
public void onNavigationModeChanged(int mode) {
- mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode);
+ mUsingThreeButtonNav = QuickStepContract.isLegacyMode(mode);
+ mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode) || (
+ mIsTrackpadGestureFeaturesEnabled && mUsingThreeButtonNav && mIsTrackpadConnected);
updateIsEnabled();
updateCurrentUserResources();
}
@@ -622,8 +681,6 @@
// Add a nav bar panel window
mIsNewBackAffordanceEnabled = mFeatureFlags.isEnabled(Flags.NEW_BACK_AFFORDANCE);
- mIsTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(
- Flags.TRACKPAD_GESTURE_FEATURES);
resetEdgeBackPlugin();
mPluginManager.addPluginListener(
this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false);
@@ -827,6 +884,12 @@
mDisplaySize.y - insets.bottom);
}
+ private boolean isTrackpadDevice(int deviceId) {
+ InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
+ return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE
+ | InputDevice.SOURCE_TOUCHPAD);
+ }
+
private boolean desktopExcludeRegionContains(int x, int y) {
return mDesktopModeExcludeRegion.contains(x, y);
}
@@ -951,17 +1014,21 @@
mMLResults = 0;
mLogGesture = false;
mInRejectedExclusion = false;
- // Trackpad back gestures don't have zones, so we don't need to check if the down event
- // is within insets. Also we don't allow back for button press from the trackpad, and
- // yet we do with a mouse.
boolean isWithinInsets = isWithinInsets((int) ev.getX(), (int) ev.getY());
- mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed
- && !isButtonPressFromTrackpad(ev)
- && (isTrackpadMultiFingerSwipe || isWithinInsets)
+ boolean isBackAllowedCommon = !mDisabledForQuickstep && mIsBackGestureAllowed
&& !mGestureBlockingActivityRunning
&& !QuickStepContract.isBackGestureDisabled(mSysUiFlags)
- && (isValidTrackpadBackGesture(isTrackpadMultiFingerSwipe)
- || isWithinTouchRegion((int) ev.getX(), (int) ev.getY()));
+ && !isTrackpadScroll(mIsTrackpadGestureFeaturesEnabled, ev);
+ if (isTrackpadMultiFingerSwipe) {
+ // Trackpad back gestures don't have zones, so we don't need to check if the down
+ // event is within insets.
+ mAllowGesture = isBackAllowedCommon && isValidTrackpadBackGesture(
+ isTrackpadMultiFingerSwipe);
+ } else {
+ mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets
+ && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
+ && !isButtonPressFromTrackpad(ev);
+ }
if (mAllowGesture) {
mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
mEdgeBackPlugin.onMotionEvent(ev);
@@ -1070,6 +1137,7 @@
}
private boolean isButtonPressFromTrackpad(MotionEvent ev) {
+ // We don't allow back for button press from the trackpad, and yet we do with a mouse.
int sources = InputManager.getInstance().getInputDevice(ev.getDeviceId()).getSources();
return (sources & (SOURCE_MOUSE | SOURCE_TOUCHPAD)) == sources && ev.getButtonState() != 0;
}
@@ -1193,6 +1261,8 @@
pw.println(" mPredictionLog=" + String.join("\n", mPredictionLog));
pw.println(" mGestureLogInsideInsets=" + String.join("\n", mGestureLogInsideInsets));
pw.println(" mGestureLogOutsideInsets=" + String.join("\n", mGestureLogOutsideInsets));
+ pw.println(" mIsTrackpadConnected=" + mIsTrackpadConnected);
+ pw.println(" mUsingThreeButtonNav=" + mUsingThreeButtonNav);
pw.println(" mEdgeBackPlugin=" + mEdgeBackPlugin);
if (mEdgeBackPlugin != null) {
mEdgeBackPlugin.dump(pw);
@@ -1242,6 +1312,7 @@
private final SysUiState mSysUiState;
private final PluginManager mPluginManager;
private final Executor mExecutor;
+ private final Handler mHandler;
private final Executor mBackgroundExecutor;
private final UserTracker mUserTracker;
private final ProtoTracer mProtoTracer;
@@ -1250,6 +1321,7 @@
private final ViewConfiguration mViewConfiguration;
private final WindowManager mWindowManager;
private final IWindowManager mWindowManagerService;
+ private final InputManager mInputManager;
private final Optional<Pip> mPipOptional;
private final Optional<DesktopMode> mDesktopModeOptional;
private final FalsingManager mFalsingManager;
@@ -1264,6 +1336,7 @@
SysUiState sysUiState,
PluginManager pluginManager,
@Main Executor executor,
+ @Main Handler handler,
@Background Executor backgroundExecutor,
UserTracker userTracker,
ProtoTracer protoTracer,
@@ -1272,6 +1345,7 @@
ViewConfiguration viewConfiguration,
WindowManager windowManager,
IWindowManager windowManagerService,
+ InputManager inputManager,
Optional<Pip> pipOptional,
Optional<DesktopMode> desktopModeOptional,
FalsingManager falsingManager,
@@ -1284,6 +1358,7 @@
mSysUiState = sysUiState;
mPluginManager = pluginManager;
mExecutor = executor;
+ mHandler = handler;
mBackgroundExecutor = backgroundExecutor;
mUserTracker = userTracker;
mProtoTracer = protoTracer;
@@ -1292,6 +1367,7 @@
mViewConfiguration = viewConfiguration;
mWindowManager = windowManager;
mWindowManagerService = windowManagerService;
+ mInputManager = inputManager;
mPipOptional = pipOptional;
mDesktopModeOptional = desktopModeOptional;
mFalsingManager = falsingManager;
@@ -1309,6 +1385,7 @@
mSysUiState,
mPluginManager,
mExecutor,
+ mHandler,
mBackgroundExecutor,
mUserTracker,
mProtoTracer,
@@ -1317,6 +1394,7 @@
mViewConfiguration,
mWindowManager,
mWindowManagerService,
+ mInputManager,
mPipOptional,
mDesktopModeOptional,
mFalsingManager,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
index 50e8aa7..bf5e442 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java
@@ -17,11 +17,18 @@
package com.android.systemui.navigationbar.gestural;
import static android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE;
+import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE;
import android.view.MotionEvent;
public final class Utilities {
+ public static boolean isTrackpadScroll(boolean isTrackpadGestureFeaturesEnabled,
+ MotionEvent event) {
+ return isTrackpadGestureFeaturesEnabled
+ && event.getClassification() == CLASSIFICATION_TWO_FINGER_SWIPE;
+ }
+
public static boolean isTrackpadMultiFingerSwipe(boolean isTrackpadGestureFeaturesEnabled,
MotionEvent event) {
return isTrackpadGestureFeaturesEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
new file mode 100644
index 0000000..3e947d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask;
+
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.UserHandle;
+
+/** A service to help with controlling the state of notes app bubble through the system user. */
+interface INoteTaskBubblesService {
+
+ boolean areBubblesAvailable();
+
+ void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
new file mode 100644
index 0000000..ec205f8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.IBinder
+import android.os.UserHandle
+import com.android.internal.infra.ServiceConnector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.DebugLogger.debugLog
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * A utility class to help interact with [Bubbles] as system user. The SysUI instance running as
+ * system user is the only instance that has the instance of [Bubbles] that manages the notes app
+ * bubble for all users.
+ *
+ * <p>Note: This class is made overridable so that a fake can be created for as mocking suspending
+ * functions is not supported by the Android tree's version of mockito.
+ */
+@SysUISingleton
+open class NoteTaskBubblesController
+@Inject
+constructor(
+ @Application private val context: Context,
+ @Background private val bgDispatcher: CoroutineDispatcher
+) {
+
+ private val serviceConnector: ServiceConnector<INoteTaskBubblesService> =
+ ServiceConnector.Impl(
+ context,
+ Intent(context, NoteTaskBubblesService::class.java),
+ Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+ UserHandle.USER_SYSTEM,
+ INoteTaskBubblesService.Stub::asInterface
+ )
+
+ /** Returns whether notes app bubble is supported. */
+ open suspend fun areBubblesAvailable(): Boolean =
+ withContext(bgDispatcher) {
+ suspendCoroutine { continuation ->
+ serviceConnector
+ .postForResult { it.areBubblesAvailable() }
+ .whenComplete { available, error ->
+ if (error != null) {
+ debugLog(error = error) { "Failed to query Bubbles as system user." }
+ }
+ continuation.resume(available ?: false)
+ }
+ }
+ }
+
+ /** Calls the [Bubbles.showOrHideAppBubble] API as [UserHandle.USER_SYSTEM]. */
+ open suspend fun showOrHideAppBubble(
+ intent: Intent,
+ userHandle: UserHandle,
+ icon: Icon
+ ) {
+ withContext(bgDispatcher) {
+ serviceConnector
+ .post { it.showOrHideAppBubble(intent, userHandle, icon) }
+ .whenComplete { _, error ->
+ if (error != null) {
+ debugLog(error = error) {
+ "Failed to show notes app bubble for intent $intent, " +
+ "user $userHandle, and icon $icon."
+ }
+ } else {
+ debugLog {
+ "Call to show notes app bubble for intent $intent, " +
+ "user $userHandle, and icon $icon successful."
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * A helper service to call [Bubbles] APIs that should always be called from the system user
+ * instance of SysUI.
+ *
+ * <p>Note: This service always runs in the SysUI process running on the system user
+ * irrespective of which user started the service. This is required so that the correct instance
+ * of {@link Bubbles} is injected. This is set via attribute {@code android:singleUser=”true”}
+ * in AndroidManifest.
+ */
+ class NoteTaskBubblesService
+ @Inject
+ constructor(private val mOptionalBubbles: Optional<Bubbles>) : Service() {
+
+ override fun onBind(intent: Intent): IBinder {
+ return object : INoteTaskBubblesService.Stub() {
+ override fun areBubblesAvailable() = mOptionalBubbles.isPresent
+
+ override fun showOrHideAppBubble(
+ intent: Intent,
+ userHandle: UserHandle,
+ icon: Icon
+ ) {
+ mOptionalBubbles.ifPresentOrElse(
+ { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) },
+ {
+ debugLog {
+ "Failed to show or hide bubble for intent $intent," +
+ "user $user, and icon $icon as bubble is empty."
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 25272ae..efbec29 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -33,25 +33,27 @@
import android.graphics.drawable.Icon
import android.os.UserHandle
import android.os.UserManager
+import android.provider.Settings
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled
import com.android.systemui.log.DebugLogger.debugLog
import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser
import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
-import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.system.ActivityManagerKt.isInForeground
import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.settings.SecureSettings
import com.android.wm.shell.bubbles.Bubble
-import com.android.wm.shell.bubbles.Bubbles
import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener
-import java.util.Optional
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
/**
* Entry point for creating and managing note.
@@ -69,13 +71,15 @@
private val shortcutManager: ShortcutManager,
private val resolver: NoteTaskInfoResolver,
private val eventLogger: NoteTaskEventLogger,
- private val optionalBubbles: Optional<Bubbles>,
+ private val noteTaskBubblesController: NoteTaskBubblesController,
private val userManager: UserManager,
private val keyguardManager: KeyguardManager,
private val activityManager: ActivityManager,
@NoteTaskEnabledKey private val isEnabled: Boolean,
private val devicePolicyManager: DevicePolicyManager,
private val userTracker: UserTracker,
+ private val secureSettings: SecureSettings,
+ @Application private val applicationScope: CoroutineScope
) {
@VisibleForTesting val infoReference = AtomicReference<NoteTaskInfo?>()
@@ -100,18 +104,6 @@
}
}
- /** Starts [LaunchNoteTaskProxyActivity] on the given [user]. */
- fun startNoteTaskProxyActivityForUser(user: UserHandle) {
- context.startActivityAsUser(
- Intent().apply {
- component =
- ComponentName(context, LaunchNoteTaskManagedProfileProxyActivity::class.java)
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- },
- user
- )
- }
-
/** Starts the notes role setting. */
fun startNotesRoleSetting(activityContext: Context, entryPoint: NoteTaskEntryPoint?) {
val user =
@@ -146,7 +138,7 @@
userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle
?: userTracker.userHandle
} else {
- userTracker.userHandle
+ secureSettings.preferredUser
}
/**
@@ -175,7 +167,19 @@
) {
if (!isEnabled) return
- val bubbles = optionalBubbles.getOrNull() ?: return
+ applicationScope.launch { awaitShowNoteTaskAsUser(entryPoint, user) }
+ }
+
+ private suspend fun awaitShowNoteTaskAsUser(
+ entryPoint: NoteTaskEntryPoint,
+ user: UserHandle,
+ ) {
+ if (!isEnabled) return
+
+ if (!noteTaskBubblesController.areBubblesAvailable()) {
+ debugLog { "Bubbles not available in the system user SysUI instance" }
+ return
+ }
// TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
if (!userManager.isUserUnlocked) return
@@ -210,7 +214,7 @@
val intent = createNoteTaskIntent(info)
val icon =
Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
- bubbles.showOrHideAppBubble(intent, user, icon)
+ noteTaskBubblesController.showOrHideAppBubble(intent, user, icon)
// App bubble logging happens on `onBubbleExpandChanged`.
debugLog { "onShowNoteTask - opened as app bubble: $info" }
}
@@ -284,15 +288,55 @@
}
/**
+ * Like [updateNoteTaskAsUser] but automatically apply to the current user and all its work
+ * profiles.
+ *
+ * @see updateNoteTaskAsUser
+ * @see UserTracker.userHandle
+ * @see UserTracker.userProfiles
+ */
+ fun updateNoteTaskForCurrentUserAndManagedProfiles() {
+ updateNoteTaskAsUser(userTracker.userHandle)
+ for (profile in userTracker.userProfiles) {
+ if (userManager.isManagedProfile(profile.id)) {
+ updateNoteTaskAsUser(profile.userHandle)
+ }
+ }
+ }
+
+ /**
* Updates all [NoteTaskController] related information, including but not exclusively the
* widget shortcut created by the [user] - by default it will use the current user.
*
+ * If the user is not current user, the update will be dispatched to run in that user's process.
+ *
* Keep in mind the shortcut API has a
* [rate limiting](https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#rate-limiting)
* and may not be updated in real-time. To reduce the chance of stale shortcuts, we run the
* function during System UI initialization.
*/
fun updateNoteTaskAsUser(user: UserHandle) {
+ if (!userManager.isUserUnlocked(user)) {
+ debugLog { "updateNoteTaskAsUser call but user locked: user=$user" }
+ return
+ }
+
+ if (user == userTracker.userHandle) {
+ updateNoteTaskAsUserInternal(user)
+ } else {
+ // TODO(b/278729185): Replace fire and forget service with a bounded service.
+ val intent = NoteTaskControllerUpdateService.createIntent(context)
+ context.startServiceAsUser(intent, user)
+ }
+ }
+
+ @InternalNoteTaskApi
+ fun updateNoteTaskAsUserInternal(user: UserHandle) {
+ if (!userManager.isUserUnlocked(user)) {
+ debugLog { "updateNoteTaskAsUserInternal call but user locked: user=$user" }
+ return
+ }
+
val packageName = roleManager.getDefaultRoleHolderAsUser(ROLE_NOTES, user)
val hasNotesRoleHolder = isEnabled && !packageName.isNullOrEmpty()
@@ -310,20 +354,20 @@
/** @see OnRoleHoldersChangedListener */
fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
if (roleName != ROLE_NOTES) return
- if (!userManager.isUserUnlocked(user)) {
- debugLog { "onRoleHoldersChanged call but user locked: role=$roleName, user=$user" }
- return
- }
- if (user == userTracker.userHandle) {
- updateNoteTaskAsUser(user)
- } else {
- // TODO(b/278729185): Replace fire and forget service with a bounded service.
- val intent = NoteTaskControllerUpdateService.createIntent(context)
- context.startServiceAsUser(intent, user)
- }
+ updateNoteTaskAsUser(user)
}
+ private val SecureSettings.preferredUser: UserHandle
+ get() {
+ val userId =
+ secureSettings.getInt(
+ Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
+ userTracker.userHandle.identifier,
+ )
+ return UserHandle.of(userId)
+ }
+
companion object {
val TAG = NoteTaskController::class.simpleName.orEmpty()
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
index 26b35cc..3e352af 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskControllerUpdateService.kt
@@ -44,7 +44,7 @@
override fun onCreate() {
super.onCreate()
// TODO(b/278729185): Replace fire and forget service with a bounded service.
- controller.updateNoteTaskAsUser(user)
+ controller.updateNoteTaskAsUserInternal(user)
stopSelf()
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index 221ff65..fe1034a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -15,7 +15,10 @@
*/
package com.android.systemui.notetask
+import android.app.role.OnRoleHoldersChangedListener
import android.app.role.RoleManager
+import android.content.Context
+import android.content.pm.UserInfo
import android.os.UserHandle
import android.view.KeyEvent
import android.view.KeyEvent.KEYCODE_N
@@ -54,6 +57,7 @@
initializeHandleSystemKey()
initializeOnRoleHoldersChanged()
initializeOnUserUnlocked()
+ initializeUserTracker()
}
/**
@@ -79,7 +83,7 @@
private fun initializeOnRoleHoldersChanged() {
roleManager.addOnRoleHoldersChangedListenerAsUser(
backgroundExecutor,
- controller::onRoleHoldersChanged,
+ callbacks,
UserHandle.ALL,
)
}
@@ -93,18 +97,41 @@
*/
private fun initializeOnUserUnlocked() {
if (keyguardUpdateMonitor.isUserUnlocked(userTracker.userId)) {
- controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
- } else {
- keyguardUpdateMonitor.registerCallback(onUserUnlockedCallback)
+ controller.updateNoteTaskForCurrentUserAndManagedProfiles()
}
+ keyguardUpdateMonitor.registerCallback(callbacks)
}
- // KeyguardUpdateMonitor.registerCallback uses a weak reference, so we need a hard reference.
- private val onUserUnlockedCallback =
- object : KeyguardUpdateMonitorCallback() {
+ private fun initializeUserTracker() {
+ userTracker.addCallback(callbacks, backgroundExecutor)
+ }
+
+ // Some callbacks use a weak reference, so we play safe and keep a hard reference to them all.
+ private val callbacks =
+ object :
+ KeyguardUpdateMonitorCallback(),
+ CommandQueue.Callbacks,
+ UserTracker.Callback,
+ OnRoleHoldersChangedListener {
+
+ override fun handleSystemKey(key: KeyEvent) {
+ key.toNoteTaskEntryPointOrNull()?.let(controller::showNoteTask)
+ }
+
+ override fun onRoleHoldersChanged(roleName: String, user: UserHandle) {
+ controller.onRoleHoldersChanged(roleName, user)
+ }
+
override fun onUserUnlocked() {
- controller.setNoteTaskShortcutEnabled(true, userTracker.userHandle)
- keyguardUpdateMonitor.removeCallback(this)
+ controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+ }
+
+ override fun onUserChanged(newUser: Int, userContext: Context) {
+ controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+ }
+
+ override fun onProfilesChanged(profiles: List<UserInfo>) {
+ controller.updateNoteTaskForCurrentUserAndManagedProfiles()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
index 109cfee..c0e688f 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -26,7 +26,6 @@
import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceModule
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
-import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -40,12 +39,12 @@
@[Binds IntoMap ClassKey(NoteTaskControllerUpdateService::class)]
fun NoteTaskControllerUpdateService.bindNoteTaskControllerUpdateService(): Service
+ @[Binds IntoMap ClassKey(NoteTaskBubblesController.NoteTaskBubblesService::class)]
+ fun NoteTaskBubblesController.NoteTaskBubblesService.bindNoteTaskBubblesService(): Service
+
@[Binds IntoMap ClassKey(LaunchNoteTaskActivity::class)]
fun LaunchNoteTaskActivity.bindNoteTaskLauncherActivity(): Activity
- @[Binds IntoMap ClassKey(LaunchNoteTaskManagedProfileProxyActivity::class)]
- fun LaunchNoteTaskManagedProfileProxyActivity.bindNoteTaskLauncherProxyActivity(): Activity
-
@[Binds IntoMap ClassKey(LaunchNotesRoleSettingsTrampolineActivity::class)]
fun LaunchNotesRoleSettingsTrampolineActivity.bindLaunchNotesRoleSettingsTrampolineActivity():
Activity
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
index 441b9f5..a2ebead 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskRoleManagerExt.kt
@@ -19,6 +19,7 @@
import android.app.role.RoleManager
import android.app.role.RoleManager.ROLE_NOTES
import android.content.Context
+import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Icon
import android.os.PersistableBundle
@@ -42,20 +43,41 @@
context: Context,
user: UserHandle,
): ShortcutInfo {
+ val packageName = getDefaultRoleHolderAsUser(ROLE_NOTES, user)
+
val extras = PersistableBundle()
- getDefaultRoleHolderAsUser(ROLE_NOTES, user)?.let { packageName ->
+ if (packageName != null) {
// Set custom app badge using the icon from ROLES_NOTES default app.
extras.putString(NoteTaskController.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE, packageName)
}
+ val shortLabel = context.getString(R.string.note_task_button_label)
+
+ val applicationLabel = context.packageManager.getApplicationLabel(packageName)
+ val longLabel =
+ if (applicationLabel == null) {
+ shortLabel
+ } else {
+ context.getString(
+ R.string.note_task_shortcut_long_label,
+ applicationLabel,
+ )
+ }
+
val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
return ShortcutInfo.Builder(context, NoteTaskController.SHORTCUT_ID)
.setIntent(LaunchNoteTaskActivity.newIntent(context = context))
- .setShortLabel(context.getString(R.string.note_task_button_label))
+ .setShortLabel(shortLabel)
+ .setLongLabel(longLabel)
.setLongLived(true)
.setIcon(icon)
.setExtras(extras)
.build()
}
+
+ private fun PackageManager.getApplicationLabel(packageName: String?): String? =
+ runCatching { getApplicationInfo(packageName, /* flags= */ 0)!! }
+ .getOrNull()
+ ?.let { info -> getApplicationLabel(info).toString() }
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
index 8ca13b9..493330a 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt
@@ -19,61 +19,18 @@
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.os.UserHandle
-import android.os.UserManager
import androidx.activity.ComponentActivity
-import com.android.systemui.log.DebugLogger.debugLog
import com.android.systemui.notetask.NoteTaskController
import com.android.systemui.notetask.NoteTaskEntryPoint
-import com.android.systemui.settings.UserTracker
import javax.inject.Inject
/** Activity responsible for launching the note experience, and finish. */
-class LaunchNoteTaskActivity
-@Inject
-constructor(
- private val controller: NoteTaskController,
- private val userManager: UserManager,
- private val userTracker: UserTracker,
-) : ComponentActivity() {
+class LaunchNoteTaskActivity @Inject constructor(private val controller: NoteTaskController) :
+ ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- // Under the hood, notes app shortcuts are shown in a floating window, called Bubble.
- // Bubble API is only available in the main user but not work profile.
- //
- // On devices with work profile (WP), SystemUI provides both personal notes app shortcuts &
- // work profile notes app shortcuts. In order to make work profile notes app shortcuts to
- // show in Bubble, a few redirections across users are required:
- // 1. When `LaunchNoteTaskActivity` is started in the work profile user, we launch
- // `LaunchNoteTaskManagedProfileProxyActivity` on the main user, which has access to the
- // Bubble API.
- // 2. `LaunchNoteTaskManagedProfileProxyActivity` calls `Bubble#showOrHideAppBubble` with
- // the work profile user ID.
- // 3. Bubble renders the work profile notes app activity in a floating window, which is
- // hosted in the main user.
- //
- // WP main user
- // ------------------------ -------------------------------------------
- // | LaunchNoteTaskActivity | -> | LaunchNoteTaskManagedProfileProxyActivity |
- // ------------------------ -------------------------------------------
- // |
- // main user |
- // ---------------------------- |
- // | Bubble#showOrHideAppBubble | <--------------
- // | (with WP user ID) |
- // ----------------------------
- val mainUser: UserHandle? = userManager.mainUser
- if (userManager.isManagedProfile) {
- if (mainUser == null) {
- debugLog { "Can't find the main user. Skipping the notes app launch." }
- } else {
- controller.startNoteTaskProxyActivityForUser(mainUser)
- }
- } else {
- controller.showNoteTask(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT)
- }
+ controller.showNoteTaskAsUser(entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT, user)
finish()
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt
deleted file mode 100644
index 3259b0d..0000000
--- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivity.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.notetask.shortcut
-
-import android.os.Build
-import android.os.Bundle
-import android.os.UserManager
-import android.util.Log
-import androidx.activity.ComponentActivity
-import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.notetask.NoteTaskEntryPoint
-import com.android.systemui.settings.UserTracker
-import javax.inject.Inject
-
-/**
- * An internal proxy activity that starts notes app in the work profile.
- *
- * If there is no work profile, this activity finishes gracefully.
- *
- * This activity MUST NOT be exported because that would expose the INTERACT_ACROSS_USER privilege
- * to any apps.
- */
-class LaunchNoteTaskManagedProfileProxyActivity
-@Inject
-constructor(
- private val controller: NoteTaskController,
- private val userTracker: UserTracker,
- private val userManager: UserManager,
-) : ComponentActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val managedProfileUser =
- userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }
-
- if (managedProfileUser == null) {
- logDebug { "Fail to find the work profile user." }
- } else {
- controller.showNoteTaskAsUser(
- entryPoint = NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT,
- user = managedProfileUser.userHandle
- )
- }
- finish()
- }
-}
-
-private inline fun logDebug(message: () -> String) {
- if (Build.IS_DEBUGGABLE) {
- Log.d(NoteTaskController.TAG, message())
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
index 380b85c..212a2b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
@@ -286,7 +286,6 @@
mHandler.removeCallbacks(mHideProgressBarRunnable);
mHandler.removeCallbacks(mHideSearchingRunnable);
mMobileNetworkLayout.setOnClickListener(null);
- mMobileDataToggle.setOnCheckedChangeListener(null);
mConnectedWifListLayout.setOnClickListener(null);
if (mSecondaryMobileNetworkLayout != null) {
mSecondaryMobileNetworkLayout.setOnClickListener(null);
@@ -351,18 +350,16 @@
}
mInternetDialogController.connectCarrierNetwork();
});
- mMobileDataToggle.setOnCheckedChangeListener(
- (buttonView, isChecked) -> {
- if (!isChecked && shouldShowMobileDialog()) {
- showTurnOffMobileDialog();
- } else if (!shouldShowMobileDialog()) {
- if (mInternetDialogController.isMobileDataEnabled() == isChecked) {
- return;
- }
- mInternetDialogController.setMobileDataEnabled(mContext, mDefaultDataSubId,
- isChecked, false);
- }
- });
+ mMobileDataToggle.setOnClickListener(v -> {
+ boolean isChecked = mMobileDataToggle.isChecked();
+ if (!isChecked && shouldShowMobileDialog()) {
+ mMobileDataToggle.setChecked(true);
+ showTurnOffMobileDialog();
+ } else if (mInternetDialogController.isMobileDataEnabled() != isChecked) {
+ mInternetDialogController.setMobileDataEnabled(mContext, mDefaultDataSubId,
+ isChecked, false);
+ }
+ });
mConnectedWifListLayout.setOnClickListener(this::onClickConnectedWifi);
mSeeAllLayout.setOnClickListener(this::onClickSeeMoreButton);
mWiFiToggle.setOnCheckedChangeListener(
@@ -696,9 +693,7 @@
mAlertDialog = new Builder(mContext)
.setTitle(R.string.mobile_data_disable_title)
.setMessage(mContext.getString(R.string.mobile_data_disable_message, carrierName))
- .setNegativeButton(android.R.string.cancel, (d, w) -> {
- mMobileDataToggle.setChecked(true);
- })
+ .setNegativeButton(android.R.string.cancel, (d, w) -> {})
.setPositiveButton(
com.android.internal.R.string.alert_windows_notification_turn_off_action,
(d, w) -> {
@@ -708,7 +703,6 @@
Prefs.putBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, true);
})
.create();
- mAlertDialog.setOnCancelListener(dialog -> mMobileDataToggle.setChecked(true));
mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
SystemUIDialog.setShowForAllUsers(mAlertDialog, true);
SystemUIDialog.registerDismissListener(mAlertDialog);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 07259c2..bb38b30 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -200,8 +200,8 @@
// TODO: change the method signature to use (boolean inputFocusTransferStarted)
@Override
- public void onStatusBarMotionEvent(MotionEvent event) {
- verifyCallerAndClearCallingIdentity("onStatusBarMotionEvent", () -> {
+ public void onStatusBarTouchEvent(MotionEvent event) {
+ verifyCallerAndClearCallingIdentity("onStatusBarTouchEvent", () -> {
// TODO move this logic to message queue
mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces -> {
if (event.getActionMasked() == ACTION_DOWN) {
@@ -236,6 +236,13 @@
}
@Override
+ public void onStatusBarTrackpadEvent(MotionEvent event) {
+ verifyCallerAndClearCallingIdentityPostMain("onStatusBarTrackpadEvent", () ->
+ mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces ->
+ centralSurfaces.onStatusBarTrackpadEvent(event)));
+ }
+
+ @Override
public void onBackPressed() {
verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> {
sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
@@ -345,7 +352,7 @@
@Override
public void expandNotificationPanel() {
- verifyCallerAndClearCallingIdentity("expandNotificationPanel",
+ verifyCallerAndClearCallingIdentityPostMain("expandNotificationPanel",
() -> mCommandQueue.handleSystemKey(new KeyEvent(KeyEvent.ACTION_DOWN,
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN)));
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
new file mode 100644
index 0000000..8f001ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -0,0 +1,7 @@
+package com.android.systemui.scene.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+
+/** A root view of the main SysUI window that supports scenes. */
+class SceneWindowRootView(context: Context?, attrs: AttributeSet?) : WindowRootView(context, attrs)
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
new file mode 100644
index 0000000..a0f9667
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
@@ -0,0 +1,8 @@
+package com.android.systemui.scene.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+/** A view that can serve as the root of the main SysUI window. */
+open class WindowRootView(context: Context?, attrs: AttributeSet?) : FrameLayout(context, attrs)
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
index afc8bff..7de22b1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
@@ -19,7 +19,7 @@
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
-import android.os.UserManager;
+import android.os.UserHandle;
import androidx.annotation.Nullable;
@@ -39,15 +39,14 @@
private final DisplayTracker mDisplayTracker;
@Inject
- AppClipsCrossProcessHelper(@Application Context context, UserManager userManager,
- DisplayTracker displayTracker) {
+ AppClipsCrossProcessHelper(@Application Context context, DisplayTracker displayTracker) {
// Start a service as main user so that even if the app clips activity is running as work
// profile user the service is able to use correct instance of Bubbles to grab a screenshot
// excluding the bubble layer.
mProxyConnector = new ServiceConnector.Impl<>(context,
new Intent(context, AppClipsScreenshotHelperService.class),
Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY
- | Context.BIND_NOT_VISIBLE, userManager.getMainUser().getIdentifier(),
+ | Context.BIND_NOT_VISIBLE, UserHandle.USER_SYSTEM,
IAppClipsScreenshotHelperService.Stub::asInterface);
mDisplayTracker = displayTracker;
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
index 83ff020..e0b9f9b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
@@ -33,6 +33,11 @@
/**
* A helper service that runs in SysUI process and helps {@link AppClipsActivity} which runs in its
* own separate process take a screenshot.
+ *
+ * <p>Note: This service always runs in the SysUI process running on the system user irrespective of
+ * which user started the service. This is required so that the correct instance of {@link Bubbles}
+ * instance is injected. This is set via attribute {@code android:singleUser=”true”} in
+ * AndroidManifest.
*/
public class AppClipsScreenshotHelperService extends Service {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
index 3949492..dce8c81 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
@@ -16,6 +16,11 @@
package com.android.systemui.screenshot.appclips;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
+
import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
import android.app.Activity;
@@ -25,17 +30,12 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.Intent.CaptureContentForNoteStatusCodes;
import android.content.res.Resources;
import android.os.IBinder;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.Log;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import com.android.internal.infra.AndroidFuture;
-import com.android.internal.infra.ServiceConnector;
import com.android.internal.statusbar.IAppClipsService;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Application;
@@ -43,73 +43,36 @@
import com.android.wm.shell.bubbles.Bubbles;
import java.util.Optional;
-import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
/**
* A service that communicates with {@link StatusBarManager} to support the
- * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API.
+ * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API. Also used by
+ * {@link AppClipsTrampolineActivity} to query if an app should be allowed to user App Clips.
+ *
+ * <p>Note: This service always runs in the SysUI process running on the system user irrespective of
+ * which user started the service. This is required so that the correct instance of {@link Bubbles}
+ * instance is injected. This is set via attribute {@code android:singleUser=”true”} in
+ * AndroidManifest.
*/
public class AppClipsService extends Service {
- private static final String TAG = AppClipsService.class.getSimpleName();
-
@Application private final Context mContext;
private final FeatureFlags mFeatureFlags;
private final Optional<Bubbles> mOptionalBubbles;
private final DevicePolicyManager mDevicePolicyManager;
- private final UserManager mUserManager;
-
private final boolean mAreTaskAndTimeIndependentPrerequisitesMet;
- @VisibleForTesting()
- @Nullable ServiceConnector<IAppClipsService> mProxyConnectorToMainProfile;
-
@Inject
public AppClipsService(@Application Context context, FeatureFlags featureFlags,
- Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager,
- UserManager userManager) {
+ Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager) {
mContext = context;
mFeatureFlags = featureFlags;
mOptionalBubbles = optionalBubbles;
mDevicePolicyManager = devicePolicyManager;
- mUserManager = userManager;
-
- // The consumer of this service are apps that call through StatusBarManager API to query if
- // it can use app clips API. Since these apps can be launched as work profile users, this
- // service will start as work profile user. SysUI doesn't share injected instances for
- // different users. This is why the bubbles instance injected will be incorrect. As the apps
- // don't generally have permission to connect to a service running as different user, we
- // start a proxy connection to communicate with the main user's version of this service.
- if (mUserManager.isManagedProfile()) {
- // No need to check for prerequisites in this case as those are incorrect for work
- // profile user instance of the service and the main user version of the service will
- // take care of this check.
- mAreTaskAndTimeIndependentPrerequisitesMet = false;
-
- // Get the main user so that we can connect to the main user's version of the service.
- UserHandle mainUser = mUserManager.getMainUser();
- if (mainUser == null) {
- // If main user is not available there isn't much we can do, no apps can use app
- // clips.
- return;
- }
-
- // Set up the connection to be used later during onBind callback.
- mProxyConnectorToMainProfile =
- new ServiceConnector.Impl<>(
- context,
- new Intent(context, AppClipsService.class),
- Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY
- | Context.BIND_NOT_VISIBLE,
- mainUser.getIdentifier(),
- IAppClipsService.Stub::asInterface);
- return;
- }
mAreTaskAndTimeIndependentPrerequisitesMet = checkIndependentVariables();
- mProxyConnectorToMainProfile = null;
}
private boolean checkIndependentVariables() {
@@ -144,40 +107,25 @@
return new IAppClipsService.Stub() {
@Override
public boolean canLaunchCaptureContentActivityForNote(int taskId) {
- // In case of managed profile, use the main user's instance of the service. Callers
- // cannot directly connect to the main user's instance as they may not have the
- // permission to interact across users.
- if (mUserManager.isManagedProfile()) {
- return canLaunchCaptureContentActivityForNoteFromMainUser(taskId);
- }
+ return canLaunchCaptureContentActivityForNoteInternal(taskId)
+ == CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+ }
+ @Override
+ @CaptureContentForNoteStatusCodes
+ public int canLaunchCaptureContentActivityForNoteInternal(int taskId) {
if (!mAreTaskAndTimeIndependentPrerequisitesMet) {
- return false;
+ return CAPTURE_CONTENT_FOR_NOTE_FAILED;
}
if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) {
- return false;
+ return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
}
- return !mDevicePolicyManager.getScreenCaptureDisabled(null);
+ return mDevicePolicyManager.getScreenCaptureDisabled(null)
+ ? CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN
+ : CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
}
};
}
-
- /** Returns whether the app clips API can be used by querying the service as the main user. */
- private boolean canLaunchCaptureContentActivityForNoteFromMainUser(int taskId) {
- if (mProxyConnectorToMainProfile == null) {
- return false;
- }
-
- try {
- AndroidFuture<Boolean> future = mProxyConnectorToMainProfile.postForResult(
- service -> service.canLaunchCaptureContentActivityForNote(taskId));
- return future.get();
- } catch (ExecutionException | InterruptedException e) {
- Log.d(TAG, "Exception from service\n" + e);
- }
-
- return false;
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
index f00803c..6e5cef4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
@@ -22,41 +22,41 @@
import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
-import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED;
import android.app.Activity;
-import android.app.admin.DevicePolicyManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
+import android.content.Intent.CaptureContentForNoteStatusCodes;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.UserInfo;
-import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.ResultReceiver;
import android.os.UserHandle;
-import android.os.UserManager;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
import com.android.internal.logging.UiEventLogger;
+import com.android.internal.statusbar.IAppClipsService;
import com.android.systemui.R;
+import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.notetask.NoteTaskController;
import com.android.systemui.notetask.NoteTaskEntryPoint;
-import com.android.systemui.settings.UserTracker;
-import com.android.wm.shell.bubbles.Bubbles;
-import java.util.Optional;
+import java.util.concurrent.Executor;
import javax.inject.Inject;
@@ -82,39 +82,57 @@
private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName();
static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI";
- @VisibleForTesting
- static final String EXTRA_USE_WP_USER = TAG + "USE_WP_USER";
static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME";
private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0);
- private final DevicePolicyManager mDevicePolicyManager;
- private final FeatureFlags mFeatureFlags;
- private final Optional<Bubbles> mOptionalBubbles;
private final NoteTaskController mNoteTaskController;
private final PackageManager mPackageManager;
- private final UserTracker mUserTracker;
private final UiEventLogger mUiEventLogger;
- private final UserManager mUserManager;
+ private final BroadcastSender mBroadcastSender;
+ @Background
+ private final Executor mBgExecutor;
+ @Main
+ private final Executor mMainExecutor;
private final ResultReceiver mResultReceiver;
+ private final ServiceConnector<IAppClipsService> mAppClipsServiceConnector;
+
+ private UserHandle mUserHandle;
private Intent mKillAppClipsBroadcastIntent;
- private UserHandle mNotesAppUser;
@Inject
- public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags,
- Optional<Bubbles> optionalBubbles, NoteTaskController noteTaskController,
- PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger,
- UserManager userManager, @Main Handler mainHandler) {
- mDevicePolicyManager = devicePolicyManager;
- mFeatureFlags = flags;
- mOptionalBubbles = optionalBubbles;
+ public AppClipsTrampolineActivity(@Application Context context,
+ NoteTaskController noteTaskController, PackageManager packageManager,
+ UiEventLogger uiEventLogger, BroadcastSender broadcastSender,
+ @Background Executor bgExecutor, @Main Executor mainExecutor,
+ @Main Handler mainHandler) {
mNoteTaskController = noteTaskController;
mPackageManager = packageManager;
- mUserTracker = userTracker;
mUiEventLogger = uiEventLogger;
- mUserManager = userManager;
+ mBroadcastSender = broadcastSender;
+ mBgExecutor = bgExecutor;
+ mMainExecutor = mainExecutor;
+
+ mResultReceiver = createResultReceiver(mainHandler);
+ mAppClipsServiceConnector = createServiceConnector(context);
+ }
+
+ /** A constructor used only for testing to verify interactions with {@link ServiceConnector}. */
+ @VisibleForTesting
+ AppClipsTrampolineActivity(ServiceConnector<IAppClipsService> appClipsServiceConnector,
+ NoteTaskController noteTaskController, PackageManager packageManager,
+ UiEventLogger uiEventLogger, BroadcastSender broadcastSender,
+ @Background Executor bgExecutor, @Main Executor mainExecutor,
+ @Main Handler mainHandler) {
+ mAppClipsServiceConnector = appClipsServiceConnector;
+ mNoteTaskController = noteTaskController;
+ mPackageManager = packageManager;
+ mUiEventLogger = uiEventLogger;
+ mBroadcastSender = broadcastSender;
+ mBgExecutor = bgExecutor;
+ mMainExecutor = mainExecutor;
mResultReceiver = createResultReceiver(mainHandler);
}
@@ -127,62 +145,62 @@
return;
}
- if (mUserManager.isManagedProfile()) {
- maybeStartActivityForWPUser();
- finish();
+ mUserHandle = getUser();
+
+ mBgExecutor.execute(() -> {
+ AndroidFuture<Integer> statusCodeFuture = mAppClipsServiceConnector.postForResult(
+ service -> service.canLaunchCaptureContentActivityForNoteInternal(getTaskId()));
+ statusCodeFuture.whenCompleteAsync(this::handleAppClipsStatusCode, mMainExecutor);
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (isFinishing() && mKillAppClipsBroadcastIntent != null) {
+ mBroadcastSender.sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF);
+ }
+
+ super.onDestroy();
+ }
+
+ private void handleAppClipsStatusCode(@CaptureContentForNoteStatusCodes int statusCode,
+ Throwable error) {
+ if (isFinishing()) {
+ // It's too late, trampoline activity is finishing or already finished. Return early.
return;
}
- if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
- finish();
+ if (error != null) {
+ Log.d(TAG, "Error querying app clips service", error);
+ setErrorResultAndFinish(statusCode);
return;
}
- if (mOptionalBubbles.isEmpty()) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
- return;
- }
+ switch (statusCode) {
+ case CAPTURE_CONTENT_FOR_NOTE_SUCCESS:
+ launchAppClipsActivity();
+ break;
- if (!mOptionalBubbles.get().isAppBubbleTaskId(getTaskId())) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
- return;
+ case CAPTURE_CONTENT_FOR_NOTE_FAILED:
+ case CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED:
+ case CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN:
+ default:
+ setErrorResultAndFinish(statusCode);
}
+ }
- if (mDevicePolicyManager.getScreenCaptureDisabled(null)) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
- return;
- }
-
- ComponentName componentName;
- try {
- componentName = ComponentName.unflattenFromString(
+ private void launchAppClipsActivity() {
+ ComponentName componentName = ComponentName.unflattenFromString(
getString(R.string.config_screenshotAppClipsActivityComponent));
- } catch (Resources.NotFoundException e) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
- return;
- }
-
- if (componentName == null || componentName.getPackageName().isEmpty()
- || componentName.getClassName().isEmpty()) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
- return;
- }
-
- mNotesAppUser = getUser();
- if (getIntent().getBooleanExtra(EXTRA_USE_WP_USER, /* defaultValue= */ false)) {
- // Get the work profile user internally instead of passing around via intent extras as
- // this activity is exported apps could potentially mess around with intent extras.
- mNotesAppUser = getWorkProfileUser().orElse(mNotesAppUser);
- }
-
String callingPackageName = getCallingPackage();
- Intent intent = new Intent().setComponent(componentName)
+
+ Intent intent = new Intent()
+ .setComponent(componentName)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver)
.putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName);
try {
- // Start the App Clips activity for the user corresponding to the notes app user.
- startActivityAsUser(intent, mNotesAppUser);
+ startActivity(intent);
// Set up the broadcast intent that will inform the above App Clips activity to finish
// when this trampoline activity is finished.
@@ -198,39 +216,6 @@
}
}
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- if (isFinishing() && mKillAppClipsBroadcastIntent != null) {
- sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF);
- }
- }
-
- private Optional<UserHandle> getWorkProfileUser() {
- return mUserTracker.getUserProfiles().stream()
- .filter(profile -> mUserManager.isManagedProfile(profile.id))
- .findFirst()
- .map(UserInfo::getUserHandle);
- }
-
- private void maybeStartActivityForWPUser() {
- UserHandle mainUser = mUserManager.getMainUser();
- if (mainUser == null) {
- setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
- return;
- }
-
- // Start the activity as the main user with activity result forwarding. Set the intent extra
- // so that the newly started trampoline activity starts the actual app clips activity as the
- // work profile user. Starting the app clips activity as the work profile user is required
- // to save the screenshot in work profile user storage and grant read permission to the URI.
- startActivityAsUser(
- new Intent(this, AppClipsTrampolineActivity.class)
- .putExtra(EXTRA_USE_WP_USER, /* value= */ true)
- .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT), mainUser);
- }
-
private void setErrorResultAndFinish(int errorCode) {
setResult(RESULT_OK,
new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode));
@@ -241,7 +226,7 @@
int callingPackageUid = 0;
try {
callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName,
- APPLICATION_INFO_FLAGS, mNotesAppUser.getIdentifier()).uid;
+ APPLICATION_INFO_FLAGS, mUserHandle.getIdentifier()).uid;
} catch (NameNotFoundException e) {
Log.d(TAG, "Couldn't find notes app UID " + e);
}
@@ -281,7 +266,7 @@
mKillAppClipsBroadcastIntent = null;
// Expand the note bubble before returning the result.
- mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mNotesAppUser);
+ mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mUserHandle);
setResult(RESULT_OK, convertedData);
finish();
}
@@ -298,11 +283,18 @@
appClipsResultReceiver.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
- ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel);
+ ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel);
parcel.recycle();
return resultReceiver;
}
+ private ServiceConnector<IAppClipsService> createServiceConnector(
+ @Application Context context) {
+ return new ServiceConnector.Impl<>(context, new Intent(context, AppClipsService.class),
+ Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY | Context.BIND_NOT_VISIBLE,
+ UserHandle.USER_SYSTEM, IAppClipsService.Stub::asInterface);
+ }
+
/** This is a test only API for mocking response from {@link AppClipsActivity}. */
@VisibleForTesting
public ResultReceiver getResultReceiverForTest() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 0c800d4..8105a145 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -103,7 +103,7 @@
private final KeyguardViewMediator mKeyguardViewMediator;
private final KeyguardBypassController mKeyguardBypassController;
private final AuthController mAuthController;
- private ViewGroup mNotificationShadeView;
+ private ViewGroup mWindowRootView;
private LayoutParams mLp;
private boolean mHasTopUi;
private boolean mHasTopUiChanged;
@@ -262,7 +262,7 @@
mLp.privateFlags |= PRIVATE_FLAG_BEHAVIOR_CONTROLLED;
mLp.insetsFlags.behavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
- mWindowManager.addView(mNotificationShadeView, mLp);
+ mWindowManager.addView(mWindowRootView, mLp);
mLpChanged.copyFrom(mLp);
onThemeChanged();
@@ -274,13 +274,13 @@
}
@Override
- public void setNotificationShadeView(ViewGroup view) {
- mNotificationShadeView = view;
+ public void setWindowRootView(ViewGroup view) {
+ mWindowRootView = view;
}
@Override
- public ViewGroup getNotificationShadeView() {
- return mNotificationShadeView;
+ public ViewGroup getWindowRootView() {
+ return mWindowRootView;
}
@Override
@@ -289,7 +289,7 @@
}
private void setKeyguardDark(boolean dark) {
- int vis = mNotificationShadeView.getSystemUiVisibility();
+ int vis = mWindowRootView.getSystemUiVisibility();
if (dark) {
vis = vis | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
vis = vis | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
@@ -297,7 +297,7 @@
vis = vis & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
vis = vis & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
}
- mNotificationShadeView.setSystemUiVisibility(vis);
+ mWindowRootView.setSystemUiVisibility(vis);
}
private void applyKeyguardFlags(NotificationShadeWindowState state) {
@@ -413,11 +413,11 @@
visible = true;
mLogger.d("Visibility forced to be true");
}
- if (mNotificationShadeView != null) {
+ if (mWindowRootView != null) {
if (visible) {
- mNotificationShadeView.setVisibility(View.VISIBLE);
+ mWindowRootView.setVisibility(View.VISIBLE);
} else {
- mNotificationShadeView.setVisibility(View.INVISIBLE);
+ mWindowRootView.setVisibility(View.INVISIBLE);
}
}
}
@@ -439,10 +439,10 @@
private void applyFitsSystemWindows(NotificationShadeWindowState state) {
boolean fitsSystemWindows = !state.isKeyguardShowingAndNotOccluded();
- if (mNotificationShadeView != null
- && mNotificationShadeView.getFitsSystemWindows() != fitsSystemWindows) {
- mNotificationShadeView.setFitsSystemWindows(fitsSystemWindows);
- mNotificationShadeView.requestApplyInsets();
+ if (mWindowRootView != null
+ && mWindowRootView.getFitsSystemWindows() != fitsSystemWindows) {
+ mWindowRootView.setFitsSystemWindows(fitsSystemWindows);
+ mWindowRootView.requestApplyInsets();
}
}
@@ -482,7 +482,7 @@
if (mDeferWindowLayoutParams == 0 && mLp != null && mLp.copyFrom(mLpChanged) != 0) {
mLogger.logApplyingWindowLayoutParams(mLp);
Trace.beginSection("updateViewLayout");
- mWindowManager.updateViewLayout(mNotificationShadeView, mLp);
+ mWindowManager.updateViewLayout(mWindowRootView, mLp);
Trace.endSection();
}
}
@@ -608,7 +608,7 @@
try {
final IWindowSession session = WindowManagerGlobal.getWindowSession();
session.updateTapExcludeRegion(
- IWindow.Stub.asInterface(getNotificationShadeView().getWindowToken()),
+ IWindow.Stub.asInterface(getWindowRootView().getWindowToken()),
region);
} catch (RemoteException e) {
Log.e(TAG, "could not update the tap exclusion region:" + e);
@@ -847,8 +847,8 @@
pw.println(" mKeyguardPreferredRefreshRate=" + mKeyguardPreferredRefreshRate);
pw.println(" mDeferWindowLayoutParams=" + mDeferWindowLayoutParams);
pw.println(mCurrentState);
- if (mNotificationShadeView != null && mNotificationShadeView.getViewRootImpl() != null) {
- mNotificationShadeView.getViewRootImpl().dump(" ", pw);
+ if (mWindowRootView != null && mWindowRootView.getViewRootImpl() != null) {
+ mWindowRootView.getViewRootImpl().dump(" ", pw);
}
new DumpsysTableLogger(
TAG,
@@ -864,7 +864,7 @@
@Override
public void onThemeChanged() {
- if (mNotificationShadeView == null) {
+ if (mWindowRootView == null) {
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index d75190e..c9122c7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -59,11 +59,14 @@
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import com.android.systemui.R;
import com.android.systemui.compose.ComposeFacade;
+import com.android.systemui.scene.ui.view.WindowRootView;
/**
- * Combined keyguard and notification panel view. Also holding backdrop and scrims.
+ * Combined keyguard and notification panel view. Also holding backdrop and scrims. This view can
+ * serve as the root view of the main SysUI window, but because other views can also serve that
+ * purpose, users of this class cannot assume it is the root.
*/
-public class NotificationShadeWindowView extends FrameLayout {
+public class NotificationShadeWindowView extends WindowRootView {
public static final String TAG = "NotificationShadeWindowView";
private int mRightInset = 0;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 2f7644e..f4485c9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -36,6 +36,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
+import com.android.keyguard.KeyguardMessageAreaController;
import com.android.keyguard.LockIconViewController;
import com.android.keyguard.dagger.KeyguardBouncerComponent;
import com.android.systemui.R;
@@ -45,12 +46,14 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
+import com.android.systemui.log.BouncerLogger;
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor;
import com.android.systemui.multishade.ui.view.MultiShadeView;
@@ -149,12 +152,15 @@
PulsingGestureListener pulsingGestureListener,
KeyguardBouncerViewModel keyguardBouncerViewModel,
KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
+ KeyguardMessageAreaController.Factory messageAreaControllerFactory,
KeyguardTransitionInteractor keyguardTransitionInteractor,
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
FeatureFlags featureFlags,
Provider<MultiShadeInteractor> multiShadeInteractorProvider,
SystemClock clock,
- Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider) {
+ Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider,
+ BouncerMessageInteractor bouncerMessageInteractor,
+ BouncerLogger bouncerLogger) {
mLockscreenShadeTransitionController = transitionController;
mFalsingCollector = falsingCollector;
mStatusBarStateController = statusBarStateController;
@@ -183,7 +189,11 @@
mView.findViewById(R.id.keyguard_bouncer_container),
keyguardBouncerViewModel,
primaryBouncerToGoneTransitionViewModel,
- keyguardBouncerComponentFactory);
+ keyguardBouncerComponentFactory,
+ messageAreaControllerFactory,
+ bouncerMessageInteractor,
+ bouncerLogger,
+ featureFlags);
collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(),
mLockscreenToDreamingTransition);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index b7551cf..44c732d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -31,7 +31,9 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.privacy.OngoingPrivacyChip
+import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
@@ -61,16 +63,34 @@
@Provides
@SysUISingleton
+ fun providesWindowRootView(
+ layoutInflater: LayoutInflater,
+ featureFlags: FeatureFlags,
+ ): WindowRootView {
+ return if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ layoutInflater.inflate(R.layout.scene_window_root, null)
+ } else {
+ layoutInflater.inflate(R.layout.super_notification_shade, null)
+ } as WindowRootView? ?: throw IllegalStateException(
+ "Window root view could not be properly inflated"
+ )
+ }
+
+ @Provides
+ @SysUISingleton
// TODO(b/277762009): Do something similar to
// {@link StatusBarWindowModule.InternalWindowView} so that only
// {@link NotificationShadeWindowViewController} can inject this view.
fun providesNotificationShadeWindowView(
- layoutInflater: LayoutInflater,
+ root: WindowRootView,
+ featureFlags: FeatureFlags,
): NotificationShadeWindowView {
- return layoutInflater.inflate(R.layout.super_notification_shade, /* root= */ null)
- as NotificationShadeWindowView?
+ if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ return root.findViewById(R.id.legacy_window_root)
+ }
+ return root as NotificationShadeWindowView?
?: throw IllegalStateException(
- "R.layout.super_notification_shade could not be properly inflated"
+ "root view not a NotificationShadeWindowView"
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 12420ff..f409f80 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -94,6 +94,7 @@
import com.android.systemui.keyguard.KeyguardIndication;
import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController;
import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.log.LogLevel;
import com.android.systemui.plugins.FalsingManager;
@@ -147,6 +148,7 @@
private final AuthController mAuthController;
private final KeyguardLogger mKeyguardLogger;
private final UserTracker mUserTracker;
+ private final BouncerMessageInteractor mBouncerMessageInteractor;
private ViewGroup mIndicationArea;
private KeyguardIndicationTextView mTopIndicationView;
private KeyguardIndicationTextView mLockScreenIndicationView;
@@ -253,7 +255,8 @@
KeyguardLogger keyguardLogger,
AlternateBouncerInteractor alternateBouncerInteractor,
AlarmManager alarmManager,
- UserTracker userTracker
+ UserTracker userTracker,
+ BouncerMessageInteractor bouncerMessageInteractor
) {
mContext = context;
mBroadcastDispatcher = broadcastDispatcher;
@@ -278,7 +281,7 @@
mScreenLifecycle.addObserver(mScreenObserver);
mAlternateBouncerInteractor = alternateBouncerInteractor;
mUserTracker = userTracker;
-
+ mBouncerMessageInteractor = bouncerMessageInteractor;
mFaceAcquiredMessageDeferral = faceHelpMessageDeferral;
mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>();
int[] msgIds = context.getResources().getIntArray(
@@ -1151,6 +1154,11 @@
msgId,
helpString);
} else if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
+ if (biometricSourceType == FINGERPRINT && !fpAuthFailed) {
+ mBouncerMessageInteractor.setFingerprintAcquisitionMessage(helpString);
+ } else if (faceAuthSoftError) {
+ mBouncerMessageInteractor.setFaceAcquisitionMessage(helpString);
+ }
mStatusBarKeyguardViewManager.setKeyguardMessage(helpString,
mInitialTextColorState);
} else if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
@@ -1206,6 +1214,8 @@
if (biometricSourceType == FACE) {
mFaceAcquiredMessageDeferral.reset();
}
+ mBouncerMessageInteractor.setFaceAcquisitionMessage(null);
+ mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null);
}
@Override
@@ -1226,6 +1236,8 @@
} else if (biometricSourceType == FINGERPRINT) {
onFingerprintAuthError(msgId, errString);
}
+ mBouncerMessageInteractor.setFaceAcquisitionMessage(null);
+ mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null);
}
private void onFaceAuthError(int msgId, String errString) {
@@ -1310,6 +1322,8 @@
showActionToUnlock();
}
}
+ mBouncerMessageInteractor.setFaceAcquisitionMessage(null);
+ mBouncerMessageInteractor.setFingerprintAcquisitionMessage(null);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 2ca0b00..47a4641 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -60,11 +60,11 @@
default void attach() {}
/** Sets the notification shade view. */
- default void setNotificationShadeView(ViewGroup view) {}
+ default void setWindowRootView(ViewGroup view) {}
/** Gets the notification shade view. */
@Nullable
- default ViewGroup getNotificationShadeView() {
+ default ViewGroup getWindowRootView() {
return null;
}
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 b81cb2b..cdc7cee 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
@@ -197,7 +197,6 @@
*/
private Set<Integer> mDebugTextUsedYPositions;
private final boolean mDebugRemoveAnimation;
- private final boolean mSimplifiedAppearFraction;
private final boolean mSensitiveRevealAnimEndabled;
private boolean mAnimatedInsets;
@@ -621,7 +620,6 @@
FeatureFlags featureFlags = Dependency.get(FeatureFlags.class);
mDebugLines = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES);
mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION);
- mSimplifiedAppearFraction = featureFlags.isEnabled(Flags.SIMPLIFIED_APPEAR_FRACTION);
mSensitiveRevealAnimEndabled = featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
setAnimatedInsetsEnabled(featureFlags.isEnabled(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS));
mSectionsManager = Dependency.get(NotificationSectionsManager.class);
@@ -1638,14 +1636,6 @@
return mAmbientState.getTrackedHeadsUpRow() != null;
}
- // TODO(b/246353296): remove it when Flags.SIMPLIFIED_APPEAR_FRACTION is removed
- public float calculateAppearFractionOld(float height) {
- float appearEndPosition = getAppearEndPosition();
- float appearStartPosition = getAppearStartPosition();
- return (height - appearStartPosition)
- / (appearEndPosition - appearStartPosition);
- }
-
/**
* @param height the height of the panel
* @return Fraction of the appear animation that has been performed. Normally follows expansion
@@ -1653,33 +1643,24 @@
* when HUN is swiped up.
*/
@FloatRange(from = -1.0, to = 1.0)
- public float simplifiedAppearFraction(float height) {
+ public float calculateAppearFraction(float height) {
if (isHeadsUpTransition()) {
// HUN is a special case because fraction can go negative if swiping up. And for now
// it must go negative as other pieces responsible for proper translation up assume
// negative value for HUN going up.
// This can't use expansion fraction as that goes only from 0 to 1. Also when
// appear fraction for HUN is 0, expansion fraction will be already around 0.2-0.3
- // and that makes translation jump immediately. Let's use old implementation for now and
- // see if we can figure out something better
- return MathUtils.constrain(calculateAppearFractionOld(height), -1, 1);
+ // and that makes translation jump immediately.
+ float appearEndPosition = getAppearEndPosition();
+ float appearStartPosition = getAppearStartPosition();
+ float hunAppearFraction = (height - appearStartPosition)
+ / (appearEndPosition - appearStartPosition);
+ return MathUtils.constrain(hunAppearFraction, -1, 1);
} else {
return mAmbientState.getExpansionFraction();
}
}
- public float calculateAppearFraction(float height) {
- if (mSimplifiedAppearFraction) {
- return simplifiedAppearFraction(height);
- } else if (mShouldUseSplitNotificationShade) {
- // for split shade we want to always use the new way of calculating appear fraction
- // because without it heads-up experience is very broken and it's less risky change
- return simplifiedAppearFraction(height);
- } else {
- return calculateAppearFractionOld(height);
- }
- }
-
public float getStackTranslation() {
return mStackTranslation;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 86bf7cb..e23ac7d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -28,6 +28,7 @@
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.view.KeyEvent;
+import android.view.MotionEvent;
import android.view.RemoteAnimationAdapter;
import android.view.View;
import android.view.ViewGroup;
@@ -266,8 +267,21 @@
boolean isPanelExpanded();
+ /**
+ * Used to dispatch initial touch events before crossing the threshold to pull down the
+ * notification shade. After that, since the launcher window is set to slippery, input
+ * frameworks take care of routing the events to the notification shade.
+ */
void onInputFocusTransfer(boolean start, boolean cancel, float velocity);
+ /**
+ * Dispatches status bar motion event to the notification shade. This is different from
+ * {@link #onInputFocusTransfer(boolean, boolean, float)} as it doesn't rely on setting the
+ * launcher window slippery to allow the frameworks to route those events after passing the
+ * initial threshold.
+ */
+ default void onStatusBarTrackpadEvent(MotionEvent event) {}
+
void animateCollapseQuickSettings();
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 0402d4f..2177bed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -91,6 +91,7 @@
import android.view.IRemoteAnimationRunner;
import android.view.IWindowManager;
import android.view.KeyEvent;
+import android.view.MotionEvent;
import android.view.ThreadedRenderer;
import android.view.View;
import android.view.ViewGroup;
@@ -428,6 +429,7 @@
public void togglePanel() {
mCommandQueueCallbacks.togglePanel();
}
+
/**
* The {@link StatusBarState} of the status bar.
*/
@@ -498,7 +500,6 @@
private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final PluginDependencyProvider mPluginDependencyProvider;
- private final KeyguardDismissUtil mKeyguardDismissUtil;
private final ExtensionController mExtensionController;
private final UserInfoControllerImpl mUserInfoControllerImpl;
private final DemoModeController mDemoModeController;
@@ -785,7 +786,6 @@
InitController initController,
@Named(TIME_TICK_HANDLER_NAME) Handler timeTickHandler,
PluginDependencyProvider pluginDependencyProvider,
- KeyguardDismissUtil keyguardDismissUtil,
ExtensionController extensionController,
UserInfoControllerImpl userInfoControllerImpl,
PhoneStatusBarPolicy phoneStatusBarPolicy,
@@ -882,7 +882,6 @@
mKeyguardViewMediatorCallback = viewMediatorCallback;
mInitController = initController;
mPluginDependencyProvider = pluginDependencyProvider;
- mKeyguardDismissUtil = keyguardDismissUtil;
mExtensionController = extensionController;
mUserInfoControllerImpl = userInfoControllerImpl;
mIconPolicy = phoneStatusBarPolicy;
@@ -1658,12 +1657,13 @@
CollapsedStatusBarFragment.class,
mCentralSurfacesComponent::createCollapsedStatusBarFragment);
+ ViewGroup windowRootView = mCentralSurfacesComponent.getWindowRootView();
mNotificationShadeWindowView = mCentralSurfacesComponent.getNotificationShadeWindowView();
mNotificationShadeWindowViewController = mCentralSurfacesComponent
.getNotificationShadeWindowViewController();
// TODO(b/277762009): Inject [NotificationShadeWindowView] directly into the controller.
// (Right now, there's a circular dependency.)
- mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
+ mNotificationShadeWindowController.setWindowRootView(windowRootView);
mNotificationShadeWindowViewController.setupExpandedStatusBar();
NotificationPanelViewController npvc =
mCentralSurfacesComponent.getNotificationPanelViewController();
@@ -1739,7 +1739,6 @@
mLightBarController.setBiometricUnlockController(mBiometricUnlockController);
mMediaManager.setBiometricUnlockController(mBiometricUnlockController);
- mKeyguardDismissUtil.setDismissHandler(this::executeWhenUnlocked);
Trace.endSection();
}
@@ -1991,6 +1990,11 @@
}
@Override
+ public void onStatusBarTrackpadEvent(MotionEvent event) {
+ mCentralSurfacesComponent.getNotificationPanelViewController().handleExternalTouch(event);
+ }
+
+ @Override
public void animateCollapseQuickSettings() {
if (mState == StatusBarState.SHADE) {
mShadeSurface.collapse(
@@ -2358,15 +2362,6 @@
mNotificationsController.resetUserExpandedStates();
}
- private void executeWhenUnlocked(OnDismissAction action, boolean requiresShadeOpen,
- boolean afterKeyguardGone) {
- if (mKeyguardStateController.isShowing() && requiresShadeOpen) {
- mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
- }
- mActivityStarter.dismissKeyguardThenExecute(action, null /* cancelAction */,
- afterKeyguardGone /* afterKeyguardGone */);
- }
-
/**
* Notify the shade controller that the current user changed
*
@@ -2836,7 +2831,11 @@
}
private void updateDozingState() {
- Trace.traceCounter(Trace.TRACE_TAG_APP, "dozing", mDozing ? 1 : 0);
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) {
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, "Dozing", 0);
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, "Dozing", String.valueOf(mDozing),
+ 0);
+ }
Trace.beginSection("CentralSurfaces#updateDozingState");
boolean keyguardVisible = mKeyguardStateController.isVisible();
@@ -3190,6 +3189,10 @@
@Override
public void onStartedWakingUp() {
+ // Between onStartedWakingUp() and onFinishedWakingUp(), the system is changing the
+ // display power mode. To avoid jank, animations should NOT run during these power
+ // mode transitions, which means that whenever possible, animations should
+ // start running during the onFinishedWakingUp() callback instead of this callback.
String tag = "CentralSurfaces#onStartedWakingUp";
DejankUtils.startDetectingBlockingIpcs(tag);
mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
@@ -3234,6 +3237,14 @@
updateVisibleToUser();
updateIsKeyguard();
+ });
+ DejankUtils.stopDetectingBlockingIpcs(tag);
+ }
+
+ @Override
+ public void onFinishedWakingUp() {
+ mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> {
+ // stopDozing() starts the LOCKSCREEN_TRANSITION_FROM_AOD animation.
mDozeServiceHost.stopDozing();
// This is intentionally below the stopDozing call above, since it avoids that we're
// unnecessarily animating the wakeUp transition. Animations should only be enabled
@@ -3247,13 +3258,7 @@
if (mScreenOffAnimationController.shouldHideLightRevealScrimOnWakeUp()) {
mShadeController.makeExpandedInvisible();
}
-
});
- DejankUtils.stopDetectingBlockingIpcs(tag);
- }
-
- @Override
- public void onFinishedWakingUp() {
mWakeUpCoordinator.setFullyAwake(true);
mWakeUpCoordinator.setWakingUp(false, false);
if (mKeyguardStateController.isOccluded()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
index 27b68f2..1c90c0d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
@@ -16,48 +16,41 @@
package com.android.systemui.statusbar.phone;
-import android.util.Log;
-
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
import javax.inject.Inject;
/**
- * Executes actions that require the screen to be unlocked. Delegates the actual handling to an
- * implementation passed via {@link #setDismissHandler}.
+ * Executes actions that require the screen to be unlocked.
*/
@SysUISingleton
public class KeyguardDismissUtil implements KeyguardDismissHandler {
- private static final String TAG = "KeyguardDismissUtil";
+ private final KeyguardStateController mKeyguardStateController;
- private volatile KeyguardDismissHandler mDismissHandler;
+ private final SysuiStatusBarStateController mStatusBarStateController;
+
+ private final ActivityStarter mActivityStarter;
@Inject
- public KeyguardDismissUtil() {
+ public KeyguardDismissUtil(KeyguardStateController keyguardStateController,
+ SysuiStatusBarStateController statusBarStateController,
+ ActivityStarter activityStarter) {
+ mKeyguardStateController = keyguardStateController;
+ mStatusBarStateController = statusBarStateController;
+ mActivityStarter = activityStarter;
}
- /** Sets the actual {@link KeyguardDismissHandler} implementation. */
- public void setDismissHandler(KeyguardDismissHandler dismissHandler) {
- mDismissHandler = dismissHandler;
- }
-
- /**
- * Executes an action that requires the screen to be unlocked.
- *
- * <p>Must be called after {@link #setDismissHandler}.
- *
- * @param requiresShadeOpen does the shade need to be forced open when hiding the keyguard?
- */
@Override
public void executeWhenUnlocked(OnDismissAction action, boolean requiresShadeOpen,
boolean afterKeyguardGone) {
- KeyguardDismissHandler dismissHandler = mDismissHandler;
- if (dismissHandler == null) {
- Log.wtf(TAG, "KeyguardDismissHandler not set.");
- action.onDismiss();
- return;
+ if (mKeyguardStateController.isShowing() && requiresShadeOpen) {
+ mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
}
- dismissHandler.executeWhenUnlocked(action, requiresShadeOpen, afterKeyguardGone);
+ mActivityStarter.dismissKeyguardThenExecute(action, null /* cancelAction */,
+ afterKeyguardGone /* afterKeyguardGone */);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 1bf63be..a34cc96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -1312,7 +1312,7 @@
@Override
public ViewRootImpl getViewRootImpl() {
- ViewGroup viewGroup = mNotificationShadeWindowController.getNotificationShadeView();
+ ViewGroup viewGroup = mNotificationShadeWindowController.getWindowRootView();
if (viewGroup != null) {
return viewGroup.getViewRootImpl();
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
index 273e783..158f961 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
@@ -20,6 +20,7 @@
import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.shade.NotificationPanelViewController;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.NotificationShadeWindowViewController;
@@ -80,8 +81,11 @@
@Scope
@interface CentralSurfacesScope {}
+ /** Creates the root view of the main SysUI window}. */
+ WindowRootView getWindowRootView();
+
/**
- * Creates a {@link NotificationShadeWindowView}.
+ * Creates or returns a {@link NotificationShadeWindowView}.
*/
NotificationShadeWindowView getNotificationShadeWindowView();
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 080be6d..12da17f 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -192,12 +192,6 @@
android:excludeFromRecents="true" />
<activity
- android:name="com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity"
- android:exported="false"
- android:permission="com.android.systemui.permission.SELF"
- android:excludeFromRecents="true" />
-
- <activity
android:name="com.android.systemui.notetask.LaunchNotesRoleSettingsTrampolineActivity"
android:exported="false"
android:permission="com.android.systemui.permission.SELF"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index 254f953..8dc1e8f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -16,6 +16,7 @@
package com.android.keyguard;
+import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static com.android.keyguard.KeyguardClockSwitch.LARGE;
@@ -189,6 +190,7 @@
assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
+ assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE);
}
@Test
@@ -198,6 +200,7 @@
assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
+ assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE);
}
@Test
@@ -212,6 +215,7 @@
// only big clock is removed at switch
assertThat(mLargeClockFrame.getParent()).isNull();
assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
+ assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE);
}
@Test
@@ -223,6 +227,7 @@
// only big clock is removed at switch
assertThat(mLargeClockFrame.getParent()).isNull();
assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
+ assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index d2e5a45..e1ba488 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -67,6 +67,7 @@
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.ActivityStarter;
@@ -216,7 +217,8 @@
mUserSwitcherController, mFeatureFlags, mGlobalSettings,
mSessionTracker, Optional.of(mSideFpsController), mFalsingA11yDelegate,
mTelephonyManager, mViewMediatorCallback, mAudioManager,
- mock(KeyguardFaceAuthInteractor.class));
+ mock(KeyguardFaceAuthInteractor.class),
+ mock(BouncerMessageInteractor.class));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
index 665246b..62a176c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationSettingsControllerTest.java
@@ -73,9 +73,9 @@
@Test
public void testShowSettingsPanel() {
- mMagnificationSettingsController.showMagnificationSettings();
+ mMagnificationSettingsController.toggleSettingsPanelVisibility();
- verify(mWindowMagnificationSettings).showSettingPanel();
+ verify(mWindowMagnificationSettings).toggleSettingsPanelVisibility();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 9c36af3..31c09b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.accessibility;
+import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
@@ -96,6 +97,7 @@
import com.google.common.util.concurrent.AtomicDouble;
import org.junit.After;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -147,8 +149,15 @@
private View.OnTouchListener mTouchListener;
private MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+ /**
+ * return whether window magnification is supported for current test context.
+ */
+ private boolean isWindowModeSupported() {
+ return getContext().getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION);
+ }
+
@Before
- public void setUp() throws RemoteException {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = Mockito.spy(getContext());
mHandler = new FakeHandler(TestableLooper.get(this).getLooper());
@@ -202,6 +211,9 @@
return null;
}).when(mSpyView).setOnTouchListener(
any(View.OnTouchListener.class));
+
+ // skip test if window magnification is not supported to prevent fail results. (b/279820875)
+ Assume.assumeTrue(isWindowModeSupported());
}
@After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
index ce96708..df08dec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java
@@ -37,6 +37,7 @@
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
+import android.graphics.Rect;
import android.os.UserHandle;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
@@ -49,6 +50,7 @@
import android.widget.CompoundButton;
import android.widget.LinearLayout;
+import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
@@ -65,7 +67,7 @@
@SmallTest
@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper
public class WindowMagnificationSettingsTest extends SysuiTestCase {
private static final int MAGNIFICATION_SIZE_SMALL = 1;
@@ -275,6 +277,39 @@
}
@Test
+ public void onScreenSizeChanged_resetPositionToRightBottomCorner() {
+ setupMagnificationCapabilityAndMode(
+ /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL,
+ /* mode= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
+ mWindowMagnificationSettings.showSettingPanel();
+
+ // move the panel to the center of draggable window bounds
+ mWindowMagnificationSettings.mParams.x =
+ mWindowMagnificationSettings.mDraggableWindowBounds.centerX();
+ mWindowMagnificationSettings.mParams.y =
+ mWindowMagnificationSettings.mDraggableWindowBounds.centerY();
+ mWindowMagnificationSettings.updateButtonViewLayoutIfNeeded();
+
+ final Rect testWindowBounds = new Rect(
+ mWindowManager.getCurrentWindowMetrics().getBounds());
+ testWindowBounds.set(testWindowBounds.left, testWindowBounds.top,
+ testWindowBounds.right + 200, testWindowBounds.bottom + 50);
+ mWindowManager.setWindowBounds(testWindowBounds);
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ mWindowMagnificationSettings.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE);
+ });
+
+ // the panel position should be reset to the bottom-right corner
+ assertEquals(
+ mWindowMagnificationSettings.mParams.x,
+ mWindowMagnificationSettings.mDraggableWindowBounds.right);
+ assertEquals(
+ mWindowMagnificationSettings.mParams.y,
+ mWindowMagnificationSettings.mDraggableWindowBounds.bottom);
+ }
+
+ @Test
public void showSettingsPanel_observerRegistered() {
setupMagnificationCapabilityAndMode(
/* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
index 38ecec0..db58074 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java
@@ -110,7 +110,7 @@
mWindowMagnification.mMagnificationSettingsControllerCallback
.onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ true);
return null;
- }).when(mMagnificationSettingsController).showMagnificationSettings();
+ }).when(mMagnificationSettingsController).toggleSettingsPanelVisibility();
doAnswer(invocation -> {
mWindowMagnification.mMagnificationSettingsControllerCallback
.onSettingsPanelVisibilityChanged(TEST_DISPLAY, /* shown= */ false);
@@ -198,7 +198,7 @@
mWindowMagnification.mWindowMagnifierCallback.onClickSettingsButton(TEST_DISPLAY);
waitForIdleSync();
- verify(mMagnificationSettingsController).showMagnificationSettings();
+ verify(mMagnificationSettingsController).toggleSettingsPanelVisibility();
verify(mA11yLogger).log(
eq(MagnificationSettingsEvent.MAGNIFICATION_SETTINGS_PANEL_OPENED));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index f31ac00..c4a0e7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -16,11 +16,16 @@
package com.android.systemui.keyguard;
+import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION;
+import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT;
import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManagerPolicyConstants.OFF_BECAUSE_OF_TIMEOUT;
import static android.view.WindowManagerPolicyConstants.OFF_BECAUSE_OF_USER;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT;
+import static com.android.systemui.keyguard.KeyguardViewMediator.DELAYED_KEYGUARD_ACTION;
+import static com.android.systemui.keyguard.KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -35,9 +40,12 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.AlarmManager;
import android.app.IActivityManager;
+import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.app.trust.TrustManager;
+import android.content.Context;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.telephony.TelephonyManager;
@@ -94,9 +102,12 @@
import com.android.systemui.util.DeviceConfigProxy;
import com.android.systemui.util.DeviceConfigProxyFake;
import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.settings.SystemSettings;
import com.android.systemui.util.time.FakeSystemClock;
import com.android.wm.shell.keyguard.KeyguardTransitions;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -156,18 +167,24 @@
private @Mock CentralSurfaces mCentralSurfaces;
private @Mock UiEventLogger mUiEventLogger;
private @Mock SessionTracker mSessionTracker;
+ private @Mock SystemSettings mSystemSettings;
+ private @Mock SecureSettings mSecureSettings;
+ private @Mock AlarmManager mAlarmManager;
+ private FakeSystemClock mSystemClock;
private FakeFeatureFlags mFeatureFlags;
+ private int mInitialUserId;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mFalsingCollector = new FalsingCollectorFake();
-
+ mSystemClock = new FakeSystemClock();
when(mLockPatternUtils.getDevicePolicyManager()).thenReturn(mDevicePolicyManager);
when(mPowerManager.newWakeLock(anyInt(), any())).thenReturn(mock(WakeLock.class));
when(mInteractionJankMonitor.begin(any(), anyInt())).thenReturn(true);
when(mInteractionJankMonitor.end(anyInt())).thenReturn(true);
+ mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager);
final ViewRootImpl testViewRoot = mock(ViewRootImpl.class);
when(testViewRoot.getView()).thenReturn(mock(View.class));
when(mStatusBarKeyguardViewManager.getViewRootImpl()).thenReturn(testViewRoot);
@@ -183,6 +200,12 @@
DejankUtils.setImmediate(true);
createAndStartViewMediator();
+ mInitialUserId = KeyguardUpdateMonitor.getCurrentUser();
+ }
+
+ @After
+ public void teardown() {
+ KeyguardUpdateMonitor.setCurrentUser(mInitialUserId);
}
@Test
@@ -369,6 +392,49 @@
}
@Test
+ public void lockAfterScreenTimeoutUsesValueFromSettings() {
+ int currentUserId = 99;
+ int userSpecificTimeout = 5999;
+ KeyguardUpdateMonitor.setCurrentUser(currentUserId);
+
+ when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+ when(mDevicePolicyManager.getMaximumTimeToLock(null, currentUserId)).thenReturn(0L);
+ when(mSecureSettings.getIntForUser(LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+ KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, currentUserId)).thenReturn(userSpecificTimeout);
+ mSystemClock.setElapsedRealtime(0L);
+ ArgumentCaptor<PendingIntent> pendingIntent = ArgumentCaptor.forClass(PendingIntent.class);
+
+ mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_TIMEOUT);
+
+ verify(mAlarmManager).setExactAndAllowWhileIdle(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+ eq(Long.valueOf(userSpecificTimeout)), pendingIntent.capture());
+ assertEquals(DELAYED_KEYGUARD_ACTION, pendingIntent.getValue().getIntent().getAction());
+ }
+
+ @Test
+ public void lockAfterSpecifiedAfterDreamStarted() {
+ int currentUserId = 99;
+ int userSpecificTimeout = 5999;
+ KeyguardUpdateMonitor.setCurrentUser(currentUserId);
+
+ // set mDeviceInteractive to true
+ mViewMediator.onStartedWakingUp(WAKE_REASON_WAKE_MOTION, false);
+ mFeatureFlags.set(Flags.LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING, false);
+ when(mLockPatternUtils.isSecure(currentUserId)).thenReturn(true);
+ when(mDevicePolicyManager.getMaximumTimeToLock(null, currentUserId)).thenReturn(0L);
+ when(mSecureSettings.getIntForUser(LOCK_SCREEN_LOCK_AFTER_TIMEOUT,
+ KEYGUARD_LOCK_AFTER_DELAY_DEFAULT, currentUserId)).thenReturn(userSpecificTimeout);
+ mSystemClock.setElapsedRealtime(0L);
+ ArgumentCaptor<PendingIntent> pendingIntent = ArgumentCaptor.forClass(PendingIntent.class);
+
+ mViewMediator.onDreamingStarted();
+
+ verify(mAlarmManager).setExactAndAllowWhileIdle(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+ eq(Long.valueOf(userSpecificTimeout)), pendingIntent.capture());
+ assertEquals(DELAYED_KEYGUARD_ACTION, pendingIntent.getValue().getIntent().getAction());
+ }
+
+ @Test
public void testHideSurfaceBehindKeyguardMarksKeyguardNotGoingAway() {
mViewMediator.hideSurfaceBehindKeyguard();
@@ -629,7 +695,10 @@
() -> mNotificationShadeWindowController,
() -> mActivityLaunchAnimator,
() -> mScrimController,
- mFeatureFlags);
+ mFeatureFlags,
+ mSecureSettings,
+ mSystemSettings,
+ mSystemClock);
mViewMediator.start();
mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
new file mode 100644
index 0000000..450aadd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import kotlinx.coroutines.CoroutineDispatcher
+
+/**
+ * Fake for [NoteTaskBubblesController] as mocking suspending functions is not supported in the
+ * Android tree's version of mockito. Ideally the [NoteTaskBubblesController] should be implemented
+ * using an interface for effectively providing multiple implementations but as this fake primarily
+ * for dealing with old version of mockito there isn't any benefit in adding complexity.
+ */
+class FakeNoteTaskBubbleController(
+ unUsed1: Context,
+ unsUsed2: CoroutineDispatcher,
+ private val optionalBubbles: Optional<Bubbles>
+) : NoteTaskBubblesController(unUsed1, unsUsed2) {
+ override suspend fun areBubblesAvailable() = optionalBubbles.isPresent
+
+ override suspend fun showOrHideAppBubble(intent: Intent, userHandle: UserHandle, icon: Icon) {
+ optionalBubbles.ifPresentOrElse(
+ { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) },
+ { throw IllegalAccessException() }
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
new file mode 100644
index 0000000..baac9e0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskBubblesController.NoteTaskBubblesService
+import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/** atest SystemUITests:NoteTaskBubblesServiceTest */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskBubblesServiceTest : SysuiTestCase() {
+
+ @Mock private lateinit var bubbles: Bubbles
+
+ private fun createServiceBinder(bubbles: Bubbles? = this.bubbles) =
+ NoteTaskBubblesService(Optional.ofNullable(bubbles)).onBind(Intent())
+ as INoteTaskBubblesService
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun areBubblesAvailable_bubblesNotNull_shouldReturnTrue() {
+ assertThat(createServiceBinder().areBubblesAvailable()).isTrue()
+ }
+
+ @Test
+ fun areBubblesAvailable_bubblesNull_shouldReturnFalse() {
+ assertThat(createServiceBinder(bubbles = null).areBubblesAvailable()).isFalse()
+ }
+
+ @Test
+ fun showOrHideAppBubble() {
+ val intent = Intent()
+ val user = UserHandle.SYSTEM
+ val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
+
+ createServiceBinder().showOrHideAppBubble(intent, user, icon)
+
+ verify(bubbles).showOrHideAppBubble(intent, user, icon)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index e99f8b6..204077b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -36,12 +36,12 @@
import android.content.pm.PackageManager
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
-import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.content.pm.UserInfo
import android.graphics.drawable.Icon
import android.os.UserHandle
import android.os.UserManager
+import android.provider.Settings
import androidx.test.ext.truth.content.IntentSubject.assertThat
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
@@ -56,22 +56,28 @@
import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
-import com.android.systemui.notetask.shortcut.LaunchNoteTaskManagedProfileProxyActivity
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.settings.SecureSettings
import com.android.wm.shell.bubbles.Bubble
import com.android.wm.shell.bubbles.Bubbles
import com.google.common.truth.Truth.assertThat
import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
@@ -80,6 +86,7 @@
import org.mockito.MockitoAnnotations
/** atest SystemUITests:NoteTaskControllerTest */
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() {
@@ -97,7 +104,10 @@
@Mock private lateinit var shortcutManager: ShortcutManager
@Mock private lateinit var activityManager: ActivityManager
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var secureSettings: SecureSettings
private val userTracker = FakeUserTracker()
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
@@ -105,7 +115,11 @@
whenever(context.getString(R.string.note_task_button_label))
.thenReturn(NOTE_TASK_SHORT_LABEL)
+ whenever(context.getString(eq(R.string.note_task_shortcut_long_label), any()))
+ .thenReturn(NOTE_TASK_LONG_LABEL)
whenever(context.packageManager).thenReturn(packageManager)
+ whenever(packageManager.getApplicationInfo(any(), any<Int>())).thenReturn(mock())
+ whenever(packageManager.getApplicationLabel(any())).thenReturn(NOTE_TASK_LONG_LABEL)
whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO)
whenever(userManager.isUserUnlocked).thenReturn(true)
whenever(userManager.isUserUnlocked(any<Int>())).thenReturn(true)
@@ -122,6 +136,7 @@
whenever(activityManager.getRunningTasks(anyInt())).thenReturn(emptyList())
whenever(userManager.isManagedProfile(workUserInfo.id)).thenReturn(true)
whenever(context.resources).thenReturn(getContext().resources)
+ whenever(secureSettings.userTracker).thenReturn(userTracker)
}
private fun createNoteTaskController(
@@ -132,7 +147,6 @@
context = context,
resolver = resolver,
eventLogger = eventLogger,
- optionalBubbles = Optional.ofNullable(bubbles),
userManager = userManager,
keyguardManager = keyguardManager,
isEnabled = isEnabled,
@@ -141,6 +155,10 @@
roleManager = roleManager,
shortcutManager = shortcutManager,
activityManager = activityManager,
+ secureSettings = secureSettings,
+ noteTaskBubblesController =
+ FakeNoteTaskBubbleController(context, testDispatcher, Optional.ofNullable(bubbles)),
+ applicationScope = testScope,
)
// region onBubbleExpandChanged
@@ -156,7 +174,7 @@
)
verify(eventLogger).logNoteTaskOpened(expectedInfo)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager)
}
@Test
@@ -171,7 +189,7 @@
)
verify(eventLogger).logNoteTaskClosed(expectedInfo)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager)
}
@Test
@@ -185,7 +203,7 @@
key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
}
@Test
@@ -199,7 +217,7 @@
key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
}
@Test
@@ -210,7 +228,7 @@
key = "any other key",
)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
}
@Test
@@ -221,7 +239,7 @@
key = Bubble.getAppBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user),
)
- verifyZeroInteractions(context, bubbles, keyguardManager, userManager, eventLogger)
+ verifyZeroInteractions(bubbles, keyguardManager, userManager, eventLogger)
}
// endregion
@@ -251,6 +269,44 @@
}
@Test
+ fun showNoteTask_defaultUserSet_shouldStartActivityWithExpectedUserAndLogUiEvent() {
+ whenever(secureSettings.getInt(eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), any()))
+ .thenReturn(10)
+ val user10 = UserHandle.of(/* userId= */ 10)
+
+ val expectedInfo =
+ NOTE_TASK_INFO.copy(
+ entryPoint = NoteTaskEntryPoint.TAIL_BUTTON,
+ isKeyguardLocked = true,
+ user = user10,
+ )
+ whenever(keyguardManager.isKeyguardLocked).thenReturn(expectedInfo.isKeyguardLocked)
+ whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(expectedInfo)
+
+ createNoteTaskController()
+ .showNoteTask(
+ entryPoint = expectedInfo.entryPoint!!,
+ )
+
+ val intentCaptor = argumentCaptor<Intent>()
+ val userCaptor = argumentCaptor<UserHandle>()
+ verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor))
+ intentCaptor.value.let { intent ->
+ assertThat(intent.action).isEqualTo(Intent.ACTION_CREATE_NOTE)
+ assertThat(intent.`package`).isEqualTo(NOTE_TASK_PACKAGE_NAME)
+ assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.flags and FLAG_ACTIVITY_MULTIPLE_TASK)
+ .isEqualTo(FLAG_ACTIVITY_MULTIPLE_TASK)
+ assertThat(intent.flags and FLAG_ACTIVITY_NEW_DOCUMENT)
+ .isEqualTo(FLAG_ACTIVITY_NEW_DOCUMENT)
+ assertThat(intent.getBooleanExtra(Intent.EXTRA_USE_STYLUS_MODE, false)).isTrue()
+ }
+ assertThat(userCaptor.value).isEqualTo(user10)
+ verify(eventLogger).logNoteTaskOpened(expectedInfo)
+ verifyZeroInteractions(bubbles)
+ }
+
+ @Test
fun showNoteTaskWithUser_keyguardIsLocked_shouldStartActivityWithExpectedUserAndLogUiEvent() {
val user10 = UserHandle.of(/* userId= */ 10)
val expectedInfo =
@@ -309,7 +365,7 @@
createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!)
// Context package name used to create bubble icon from drawable resource id
- verify(context).packageName
+ verify(context, atLeastOnce()).packageName
verifyNoteTaskOpenInBubbleInUser(userTracker.userHandle)
verifyZeroInteractions(eventLogger)
}
@@ -318,7 +374,7 @@
fun showNoteTask_bubblesIsNull_shouldDoNothing() {
createNoteTaskController(bubbles = null).showNoteTask(entryPoint = TAIL_BUTTON)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -330,14 +386,14 @@
noteTaskController.showNoteTask(entryPoint = TAIL_BUTTON)
verify(noteTaskController).showNoDefaultNotesAppToast()
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
fun showNoteTask_flagDisabled_shouldDoNothing() {
createNoteTaskController(isEnabled = false).showNoteTask(entryPoint = TAIL_BUTTON)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -346,7 +402,7 @@
createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -466,7 +522,7 @@
createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -482,7 +538,7 @@
createNoteTaskController().showNoteTask(entryPoint = QUICK_AFFORDANCE)
- verifyZeroInteractions(context, bubbles, eventLogger)
+ verifyZeroInteractions(bubbles, eventLogger)
}
@Test
@@ -594,11 +650,11 @@
createNoteTaskController(isEnabled = true).onRoleHoldersChanged("NOT_NOTES", user)
- verifyZeroInteractions(context)
+ verify(context, never()).startActivityAsUser(any(), any())
}
@Test
- fun onRoleHoldersChanged_notesRole_sameUser_shouldUpdateShortcuts() {
+ fun onRoleHoldersChanged_notesRole_shouldUpdateShortcuts() {
val user = userTracker.userHandle
val controller = spy(createNoteTaskController())
doNothing().whenever(controller).updateNoteTaskAsUser(any())
@@ -607,22 +663,41 @@
verify(controller).updateNoteTaskAsUser(user)
}
-
- @Test
- fun onRoleHoldersChanged_notesRole_differentUser_shouldUpdateShortcutsInUserProcess() {
- // FakeUserTracker will default to UserHandle.SYSTEM.
- val user = UserHandle.CURRENT
-
- createNoteTaskController(isEnabled = true).onRoleHoldersChanged(ROLE_NOTES, user)
-
- verify(context).startServiceAsUser(any(), eq(user))
- }
// endregion
// region updateNoteTaskAsUser
@Test
- fun updateNoteTaskAsUser_withNotesRole_withShortcuts_shouldUpdateShortcuts() {
- createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle)
+ fun updateNoteTaskAsUser_sameUser_shouldUpdateShortcuts() {
+ val user = userTracker.userHandle
+ val controller = spy(createNoteTaskController())
+ doNothing().whenever(controller).updateNoteTaskAsUserInternal(any())
+
+ controller.updateNoteTaskAsUser(user)
+
+ verify(controller).updateNoteTaskAsUserInternal(user)
+ verify(context, never()).startServiceAsUser(any(), any())
+ }
+
+ @Test
+ fun updateNoteTaskAsUser_differentUser_shouldUpdateShortcutsInUserProcess() {
+ // FakeUserTracker will default to UserHandle.SYSTEM.
+ val user = UserHandle.CURRENT
+ val controller = spy(createNoteTaskController(isEnabled = true))
+ doNothing().whenever(controller).updateNoteTaskAsUserInternal(any())
+
+ controller.updateNoteTaskAsUser(user)
+
+ verify(controller, never()).updateNoteTaskAsUserInternal(any())
+ val intent = withArgCaptor { verify(context).startServiceAsUser(capture(), eq(user)) }
+ assertThat(intent).hasComponentClass(NoteTaskControllerUpdateService::class.java)
+ }
+ // endregion
+
+ // region internalUpdateNoteTaskAsUser
+ @Test
+ fun updateNoteTaskAsUserInternal_withNotesRole_withShortcuts_shouldUpdateShortcuts() {
+ createNoteTaskController(isEnabled = true)
+ .updateNoteTaskAsUserInternal(userTracker.userHandle)
val actualComponent = argumentCaptor<ComponentName>()
verify(context.packageManager)
@@ -635,27 +710,29 @@
.isEqualTo(CreateNoteTaskShortcutActivity::class.java.name)
verify(shortcutManager, never()).disableShortcuts(any())
verify(shortcutManager).enableShortcuts(listOf(SHORTCUT_ID))
- val actualShortcuts = argumentCaptor<List<ShortcutInfo>>()
- verify(shortcutManager).updateShortcuts(actualShortcuts.capture())
- val actualShortcut = actualShortcuts.value.first()
- assertThat(actualShortcut.id).isEqualTo(SHORTCUT_ID)
- assertThat(actualShortcut.intent).run {
- hasComponentClass(LaunchNoteTaskActivity::class.java)
- hasAction(ACTION_CREATE_NOTE)
+ val shortcutInfo = withArgCaptor { verify(shortcutManager).updateShortcuts(capture()) }
+ with(shortcutInfo.first()) {
+ assertThat(id).isEqualTo(SHORTCUT_ID)
+ assertThat(intent).run {
+ hasComponentClass(LaunchNoteTaskActivity::class.java)
+ hasAction(ACTION_CREATE_NOTE)
+ }
+ assertThat(shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL)
+ assertThat(longLabel).isEqualTo(NOTE_TASK_LONG_LABEL)
+ assertThat(isLongLived).isEqualTo(true)
+ assertThat(icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget)
+ assertThat(extras?.getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE))
+ .isEqualTo(NOTE_TASK_PACKAGE_NAME)
}
- assertThat(actualShortcut.shortLabel).isEqualTo(NOTE_TASK_SHORT_LABEL)
- assertThat(actualShortcut.isLongLived).isEqualTo(true)
- assertThat(actualShortcut.icon.resId).isEqualTo(R.drawable.ic_note_task_shortcut_widget)
- assertThat(actualShortcut.extras?.getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE))
- .isEqualTo(NOTE_TASK_PACKAGE_NAME)
}
@Test
- fun updateNoteTaskAsUser_noNotesRole_shouldDisableShortcuts() {
+ fun updateNoteTaskAsUserInternal_noNotesRole_shouldDisableShortcuts() {
whenever(roleManager.getRoleHoldersAsUser(ROLE_NOTES, userTracker.userHandle))
.thenReturn(emptyList())
- createNoteTaskController(isEnabled = true).updateNoteTaskAsUser(userTracker.userHandle)
+ createNoteTaskController(isEnabled = true)
+ .updateNoteTaskAsUserInternal(userTracker.userHandle)
val argument = argumentCaptor<ComponentName>()
verify(context.packageManager)
@@ -672,8 +749,9 @@
}
@Test
- fun updateNoteTaskAsUser_flagDisabled_shouldDisableShortcuts() {
- createNoteTaskController(isEnabled = false).updateNoteTaskAsUser(userTracker.userHandle)
+ fun updateNoteTaskAsUserInternal_flagDisabled_shouldDisableShortcuts() {
+ createNoteTaskController(isEnabled = false)
+ .updateNoteTaskAsUserInternal(userTracker.userHandle)
val argument = argumentCaptor<ComponentName>()
verify(context.packageManager)
@@ -690,18 +768,17 @@
}
// endregion
- // startregion startNoteTaskProxyActivityForUser
+ // startregion updateNoteTaskForAllUsers
@Test
- fun startNoteTaskProxyActivityForUser_shouldStartLaunchNoteTaskProxyActivityWithExpectedUser() {
- val user0 = UserHandle.of(0)
- createNoteTaskController().startNoteTaskProxyActivityForUser(user0)
+ fun updateNoteTaskForAllUsers_shouldRunUpdateForCurrentUserAndProfiles() {
+ userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
+ val controller = spy(createNoteTaskController())
+ doNothing().whenever(controller).updateNoteTaskAsUser(any())
- val intentCaptor = argumentCaptor<Intent>()
- verify(context).startActivityAsUser(intentCaptor.capture(), eq(user0))
- assertThat(intentCaptor.value).run {
- hasComponentClass(LaunchNoteTaskManagedProfileProxyActivity::class.java)
- hasFlags(FLAG_ACTIVITY_NEW_TASK)
- }
+ controller.updateNoteTaskForCurrentUserAndManagedProfiles()
+
+ verify(controller).updateNoteTaskAsUser(mainUserInfo.userHandle)
+ verify(controller).updateNoteTaskAsUser(workUserInfo.userHandle)
}
// endregion
@@ -821,7 +898,8 @@
// endregion
private companion object {
- const val NOTE_TASK_SHORT_LABEL = "Notetaking"
+ const val NOTE_TASK_SHORT_LABEL = "Note-taking"
+ const val NOTE_TASK_LONG_LABEL = "Note-taking, App"
const val NOTE_TASK_ACTIVITY_NAME = "NoteTaskActivity"
const val NOTE_TASK_PACKAGE_NAME = "com.android.note.app"
const val NOTE_TASK_UID = 123456
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 4e85b6c..95bb3e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -32,9 +32,12 @@
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock
import com.android.wm.shell.bubbles.Bubbles
+import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
@@ -71,19 +74,19 @@
}
private fun createUnderTest(
- isEnabled: Boolean,
- bubbles: Bubbles?,
+ isEnabled: Boolean,
+ bubbles: Bubbles?,
): NoteTaskInitializer =
- NoteTaskInitializer(
- controller = controller,
- commandQueue = commandQueue,
- optionalBubbles = Optional.ofNullable(bubbles),
- isEnabled = isEnabled,
- roleManager = roleManager,
- userTracker = userTracker,
- keyguardUpdateMonitor = keyguardMonitor,
- backgroundExecutor = executor,
- )
+ NoteTaskInitializer(
+ controller = controller,
+ commandQueue = commandQueue,
+ optionalBubbles = Optional.ofNullable(bubbles),
+ isEnabled = isEnabled,
+ roleManager = roleManager,
+ userTracker = userTracker,
+ keyguardUpdateMonitor = keyguardMonitor,
+ backgroundExecutor = executor,
+ )
@Test
fun initialize_withUserUnlocked() {
@@ -93,8 +96,8 @@
verify(commandQueue).addCallback(any())
verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
- verify(controller).setNoteTaskShortcutEnabled(any(), any())
- verify(keyguardMonitor, never()).registerCallback(any())
+ verify(controller).updateNoteTaskForCurrentUserAndManagedProfiles()
+ verify(keyguardMonitor).registerCallback(any())
}
@Test
@@ -107,6 +110,7 @@
verify(roleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any())
verify(controller, never()).setNoteTaskShortcutEnabled(any(), any())
verify(keyguardMonitor).registerCallback(any())
+ assertThat(userTracker.callbacks).isNotEmpty()
}
@Test
@@ -116,12 +120,12 @@
underTest.initialize()
verifyZeroInteractions(
- commandQueue,
- bubbles,
- controller,
- roleManager,
- userManager,
- keyguardMonitor,
+ commandQueue,
+ bubbles,
+ controller,
+ roleManager,
+ userManager,
+ keyguardMonitor,
)
}
@@ -132,12 +136,12 @@
underTest.initialize()
verifyZeroInteractions(
- commandQueue,
- bubbles,
- controller,
- roleManager,
- userManager,
- keyguardMonitor,
+ commandQueue,
+ bubbles,
+ controller,
+ roleManager,
+ userManager,
+ keyguardMonitor,
)
}
@@ -146,7 +150,7 @@
val expectedKeyEvent = KeyEvent(ACTION_DOWN, KEYCODE_STYLUS_BUTTON_TAIL)
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
underTest.initialize()
- val callback = captureArgument { verify(commandQueue).addCallback(capture()) }
+ val callback = withArgCaptor { verify(commandQueue).addCallback(capture()) }
callback.handleSystemKey(expectedKeyEvent)
@@ -154,31 +158,49 @@
}
@Test
- fun initialize_userUnlocked() {
+ fun initialize_userUnlocked_shouldUpdateNoteTask() {
whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(false)
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
underTest.initialize()
- val callback = captureArgument { verify(keyguardMonitor).registerCallback(capture()) }
+ val callback = withArgCaptor { verify(keyguardMonitor).registerCallback(capture()) }
whenever(keyguardMonitor.isUserUnlocked(userTracker.userId)).thenReturn(true)
callback.onUserUnlocked()
- verify(controller).setNoteTaskShortcutEnabled(any(), any())
+
+ verify(controller).updateNoteTaskForCurrentUserAndManagedProfiles()
}
@Test
- fun initialize_onRoleHoldersChanged() {
+ fun initialize_onRoleHoldersChanged_shouldRunOnRoleHoldersChanged() {
val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
underTest.initialize()
- val callback = captureArgument {
+ val callback = withArgCaptor {
verify(roleManager)
- .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL))
+ .addOnRoleHoldersChangedListenerAsUser(any(), capture(), eq(UserHandle.ALL))
}
callback.onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle)
verify(controller).onRoleHoldersChanged(ROLE_NOTES, userTracker.userHandle)
}
-}
-private inline fun <reified T : Any> captureArgument(block: ArgumentCaptor<T>.() -> Unit) =
- argumentCaptor<T>().apply(block).value
+ @Test
+ fun initialize_onProfilesChanged_shouldUpdateNoteTask() {
+ val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
+ underTest.initialize()
+
+ userTracker.callbacks.first().onProfilesChanged(emptyList())
+
+ verify(controller, times(2)).updateNoteTaskForCurrentUserAndManagedProfiles()
+ }
+
+ @Test
+ fun initialize_onUserChanged_shouldUpdateNoteTask() {
+ val underTest = createUnderTest(isEnabled = true, bubbles = bubbles)
+ underTest.initialize()
+
+ userTracker.callbacks.first().onUserChanged(0, mock())
+
+ verify(controller, times(2)).updateNoteTaskForCurrentUserAndManagedProfiles()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt
index a0c376f..627c4a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt
@@ -17,9 +17,6 @@
package com.android.systemui.notetask.shortcut
import android.content.Intent
-import android.content.pm.UserInfo
-import android.os.UserHandle
-import android.os.UserManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
@@ -29,17 +26,14 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.notetask.NoteTaskController
import com.android.systemui.notetask.NoteTaskEntryPoint
-import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
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
-import org.mockito.Mockito.never
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@@ -48,8 +42,6 @@
class LaunchNoteTaskActivityTest : SysuiTestCase() {
@Mock lateinit var noteTaskController: NoteTaskController
- @Mock lateinit var userManager: UserManager
- private val userTracker: FakeUserTracker = FakeUserTracker()
@Rule
@JvmField
@@ -60,8 +52,6 @@
override fun create(intent: Intent?) =
LaunchNoteTaskActivity(
controller = noteTaskController,
- userManager = userManager,
- userTracker = userTracker
)
},
/* initialTouchMode= */ false,
@@ -71,7 +61,6 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true)
}
@After
@@ -83,36 +72,7 @@
fun startActivityOnNonWorkProfileUser_shouldLaunchNoteTask() {
activityRule.launchActivity(/* startIntent= */ null)
- verify(noteTaskController).showNoteTask(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT))
- }
-
- @Test
- fun startActivityOnWorkProfileUser_shouldLaunchProxyActivity() {
- val mainUserHandle: UserHandle = mainUser.userHandle
- userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1)
- whenever(userManager.isManagedProfile).thenReturn(true)
- whenever(userManager.mainUser).thenReturn(mainUserHandle)
-
- activityRule.launchActivity(/* startIntent= */ null)
-
- verify(noteTaskController).startNoteTaskProxyActivityForUser(eq(mainUserHandle))
- }
-
- @Test
- fun startActivityOnWorkProfileUser_noMainUser_shouldNotLaunch() {
- userTracker.set(listOf(mainUser, workProfileUser), selectedUserIndex = 1)
- whenever(userManager.isManagedProfile).thenReturn(true)
- whenever(userManager.mainUser).thenReturn(null)
-
- activityRule.launchActivity(/* startIntent= */ null)
-
- verify(noteTaskController, never()).showNoteTask(any())
- verify(noteTaskController, never()).startNoteTaskProxyActivityForUser(any())
- }
-
- private companion object {
- val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN)
- val workProfileUser =
- UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE)
+ verify(noteTaskController)
+ .showNoteTaskAsUser(eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT), any())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt
deleted file mode 100644
index 6347c34..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskManagedProfileProxyActivityTest.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.notetask.shortcut
-
-import android.content.Intent
-import android.content.pm.UserInfo
-import android.os.UserHandle
-import android.os.UserManager
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import androidx.test.rule.ActivityTestRule
-import androidx.test.runner.intercepting.SingleActivityFactory
-import com.android.dx.mockito.inline.extended.ExtendedMockito.never
-import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.notetask.NoteTaskController
-import com.android.systemui.notetask.NoteTaskEntryPoint
-import com.android.systemui.settings.FakeUserTracker
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
-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
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-@TestableLooper.RunWithLooper
-class LaunchNoteTaskManagedProfileProxyActivityTest : SysuiTestCase() {
-
- @Mock lateinit var noteTaskController: NoteTaskController
- @Mock lateinit var userManager: UserManager
- private val userTracker = FakeUserTracker()
-
- @Rule
- @JvmField
- val activityRule =
- ActivityTestRule<LaunchNoteTaskManagedProfileProxyActivity>(
- /* activityFactory= */ object :
- SingleActivityFactory<LaunchNoteTaskManagedProfileProxyActivity>(
- LaunchNoteTaskManagedProfileProxyActivity::class.java
- ) {
- override fun create(intent: Intent?) =
- LaunchNoteTaskManagedProfileProxyActivity(
- controller = noteTaskController,
- userManager = userManager,
- userTracker = userTracker
- )
- },
- /* initialTouchMode= */ false,
- /* launchActivity= */ false,
- )
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- whenever(userManager.isManagedProfile(eq(workProfileUser.id))).thenReturn(true)
- }
-
- @After
- fun tearDown() {
- activityRule.finishActivity()
- }
-
- @Test
- fun startActivity_noWorkProfileUser_shouldNotLaunchNoteTask() {
- userTracker.set(listOf(mainUser), selectedUserIndex = 0)
- activityRule.launchActivity(/* startIntent= */ null)
-
- verify(noteTaskController, never()).showNoteTaskAsUser(any(), any())
- }
-
- @Test
- fun startActivity_hasWorkProfileUser_shouldLaunchNoteTaskOnTheWorkProfileUser() {
- userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUser))
- activityRule.launchActivity(/* startIntent= */ null)
-
- val workProfileUserHandle: UserHandle = workProfileUser.userHandle
- verify(noteTaskController)
- .showNoteTaskAsUser(
- eq(NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT),
- eq(workProfileUserHandle)
- )
- }
-
- private companion object {
- val mainUser = UserInfo(/* id= */ 0, /* name= */ "primary", /* flags= */ UserInfo.FLAG_MAIN)
- val workProfileUser =
- UserInfo(/* id= */ 10, /* name= */ "work", /* flags= */ UserInfo.FLAG_PROFILE)
- val mainAndWorkProfileUsers = listOf(mainUser, workProfileUser)
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index 3def6ba..f2812b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -16,9 +16,11 @@
package com.android.systemui.screenrecord;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
@@ -32,6 +34,7 @@
import android.os.Binder;
import android.os.Handler;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
@@ -41,7 +44,9 @@
import com.android.systemui.media.MediaProjectionCaptureTarget;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.UserContextProvider;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
import org.junit.Before;
import org.junit.Test;
@@ -77,20 +82,38 @@
private UserContextProvider mUserContextTracker;
@Captor
private ArgumentCaptor<Runnable> mRunnableCaptor;
- private KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil() {
- public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
- boolean requiresShadeOpen) {
- action.onDismiss();
- }
- };
+ @Mock
+ private KeyguardStateController mKeyguardStateController;
+ @Mock
+ private SysuiStatusBarStateController mStatusBarStateController;
+ @Mock
+ private ActivityStarter mActivityStarter;
+
+ private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+
+ private KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil(
+ mKeyguardStateController, mStatusBarStateController, mActivityStarter);
private RecordingService mRecordingService;
+ private class RecordingServiceTestable extends RecordingService {
+ RecordingServiceTestable(
+ RecordingController controller, Executor executor,
+ Handler handler, UiEventLogger uiEventLogger,
+ NotificationManager notificationManager,
+ UserContextProvider userContextTracker, KeyguardDismissUtil keyguardDismissUtil) {
+ super(controller, executor, handler,
+ uiEventLogger, notificationManager, userContextTracker, keyguardDismissUtil);
+ attachBaseContext(mContext);
+ }
+ }
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mRecordingService = Mockito.spy(new RecordingService(mController, mExecutor, mHandler,
- mUiEventLogger, mNotificationManager, mUserContextTracker, mKeyguardDismissUtil));
+ mRecordingService = Mockito.spy(new RecordingServiceTestable(mController, mExecutor,
+ mHandler, mUiEventLogger, mNotificationManager,
+ mUserContextTracker, mKeyguardDismissUtil));
// Return actual context info
doReturn(mContext).when(mRecordingService).getApplicationContext();
@@ -160,8 +183,7 @@
Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, null);
mRecordingService.onStartCommand(startIntent, 0, 0);
- // Then the state is set to not recording
- verify(mController).updateState(false);
+ assertUpdateState(false);
}
@Test
@@ -179,7 +201,7 @@
mRecordingService.onStopped();
- verify(mController).updateState(false);
+ assertUpdateState(false);
}
@Test
@@ -224,8 +246,21 @@
verify(mExecutor).execute(mRunnableCaptor.capture());
mRunnableCaptor.getValue().run();
- // Then the state is set to not recording and we cancel the notification
- verify(mController).updateState(false);
+ assertUpdateState(false);
verify(mNotificationManager).cancelAsUser(any(), anyInt(), any());
}
+
+ private void assertUpdateState(boolean state) {
+ // Then the state is set to not recording, and we cancel the notification
+ // non SYSTEM user doesn't have the reference to the correct controller,
+ // so a broadcast is sent in case of non SYSTEM user.
+ if (UserHandle.USER_SYSTEM == mContext.getUserId()) {
+ verify(mController).updateState(state);
+ } else {
+ ArgumentCaptor<Intent> argumentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mRecordingService).sendBroadcast(argumentCaptor.capture(), eq(PERMISSION_SELF));
+ assertEquals(RecordingController.INTENT_UPDATE_STATE,
+ argumentCaptor.getValue().getAction());
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
index 67b1099..d8897e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
@@ -16,14 +16,17 @@
package com.android.systemui.screenshot.appclips;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
+
import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.admin.DevicePolicyManager;
@@ -31,8 +34,6 @@
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
-import android.os.UserHandle;
-import android.os.UserManager;
import androidx.test.runner.AndroidJUnit4;
@@ -46,7 +47,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
@@ -63,7 +63,6 @@
@Mock private Optional<Bubbles> mOptionalBubbles;
@Mock private Bubbles mBubbles;
@Mock private DevicePolicyManager mDevicePolicyManager;
- @Mock private UserManager mUserManager;
private AppClipsService mAppClipsService;
@@ -81,51 +80,84 @@
}
@Test
+ public void flagOff_internal_shouldReturnFailed() throws RemoteException {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false);
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ }
+
+ @Test
public void emptyBubbles_shouldReturnFalse() throws RemoteException {
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
- when(mOptionalBubbles.isEmpty()).thenReturn(true);
+ mockForEmptyBubbles();
assertThat(getInterfaceWithRealContext()
.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
}
@Test
+ public void emptyBubbles_internal_shouldReturnFailed() throws RemoteException {
+ mockForEmptyBubbles();
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ }
+
+ @Test
public void taskIdNotAppBubble_shouldReturnFalse() throws RemoteException {
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
- when(mOptionalBubbles.isEmpty()).thenReturn(false);
- when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false);
+ mockForTaskIdNotAppBubble();
assertThat(getInterfaceWithRealContext()
.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
}
@Test
+ public void taskIdNotAppBubble_internal_shouldReturnWindowUnsupported() throws RemoteException {
+ mockForTaskIdNotAppBubble();
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
+ }
+
+ @Test
public void dpmScreenshotBlocked_shouldReturnFalse() throws RemoteException {
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
- when(mOptionalBubbles.isEmpty()).thenReturn(false);
- when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
- when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+ mockForScreenshotBlocked();
assertThat(getInterfaceWithRealContext()
.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
}
@Test
+ public void dpmScreenshotBlocked_internal_shouldReturnBlockedByAdmin() throws RemoteException {
+ mockForScreenshotBlocked();
+
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
+ }
+
+ @Test
public void configComponentNameNotValid_shouldReturnFalse() throws RemoteException {
- when(mMockContext.getString(anyInt())).thenReturn(EMPTY);
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
- when(mOptionalBubbles.isEmpty()).thenReturn(false);
- when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
- when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+ mockForInvalidConfigComponentName();
assertThat(getInterfaceWithMockContext()
.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
}
@Test
+ public void configComponentNameNotValid_internal_shouldReturnFailed() throws RemoteException {
+ mockForInvalidConfigComponentName();
+
+ assertThat(getInterfaceWithMockContext()
+ .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ }
+
+
+ @Test
public void allPrerequisitesSatisfy_shouldReturnTrue() throws RemoteException {
mockToSatisfyAllPrerequisites();
@@ -134,28 +166,44 @@
}
@Test
- public void isManagedProfile_shouldUseProxyConnection() throws RemoteException {
- when(mUserManager.isManagedProfile()).thenReturn(true);
- when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM);
- IAppClipsService service = getInterfaceWithRealContext();
- mAppClipsService.mProxyConnectorToMainProfile =
- Mockito.spy(mAppClipsService.mProxyConnectorToMainProfile);
+ public void allPrerequisitesSatisfy_internal_shouldReturnSuccess() throws RemoteException {
+ mockToSatisfyAllPrerequisites();
- service.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID);
-
- verify(mAppClipsService.mProxyConnectorToMainProfile).postForResult(any());
+ assertThat(getInterfaceWithRealContext()
+ .canLaunchCaptureContentActivityForNoteInternal(FAKE_TASK_ID))
+ .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
}
- @Test
- public void isManagedProfile_noMainUser_shouldReturnFalse() {
- when(mUserManager.isManagedProfile()).thenReturn(true);
- when(mUserManager.getMainUser()).thenReturn(null);
-
- getInterfaceWithRealContext();
-
- assertThat(mAppClipsService.mProxyConnectorToMainProfile).isNull();
+ private void mockForEmptyBubbles() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(true);
}
+ private void mockForTaskIdNotAppBubble() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false);
+ }
+
+ private void mockForScreenshotBlocked() {
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+ when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+ }
+
+ private void mockForInvalidConfigComponentName() {
+ when(mMockContext.getString(anyInt())).thenReturn(EMPTY);
+ when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+ when(mOptionalBubbles.isEmpty()).thenReturn(false);
+ when(mOptionalBubbles.get()).thenReturn(mBubbles);
+ when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+ when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+ }
+
+
private void mockToSatisfyAllPrerequisites() {
when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
when(mOptionalBubbles.isEmpty()).thenReturn(false);
@@ -166,13 +214,13 @@
private IAppClipsService getInterfaceWithRealContext() {
mAppClipsService = new AppClipsService(getContext(), mFeatureFlags,
- mOptionalBubbles, mDevicePolicyManager, mUserManager);
+ mOptionalBubbles, mDevicePolicyManager);
return getInterfaceFromService(mAppClipsService);
}
private IAppClipsService getInterfaceWithMockContext() {
mAppClipsService = new AppClipsService(mMockContext, mFeatureFlags,
- mOptionalBubbles, mDevicePolicyManager, mUserManager);
+ mOptionalBubbles, mDevicePolicyManager);
return getInterfaceFromService(mAppClipsService);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
index e9007ff..7fad972 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
@@ -24,22 +24,19 @@
import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
-import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+import static com.android.internal.infra.AndroidFuture.completedFuture;
import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED;
import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
-import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_USE_WP_USER;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Activity;
-import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -52,20 +49,22 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.UserHandle;
-import android.os.UserManager;
import android.testing.AndroidTestingRunner;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.intercepting.SingleActivityFactory;
+import com.android.internal.infra.ServiceConnector;
import com.android.internal.logging.UiEventLogger;
+import com.android.internal.statusbar.IAppClipsService;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.notetask.NoteTaskController;
-import com.android.systemui.settings.UserTracker;
-import com.android.wm.shell.bubbles.Bubbles;
+
+import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Before;
@@ -75,8 +74,7 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.List;
-import java.util.Optional;
+import java.util.concurrent.Executor;
@RunWith(AndroidTestingRunner.class)
public final class AppClipsTrampolineActivityTest extends SysuiTestCase {
@@ -86,25 +84,19 @@
private static final int TEST_UID = 42;
private static final String TEST_CALLING_PACKAGE = "test-calling-package";
- @Mock
- private DevicePolicyManager mDevicePolicyManager;
- @Mock
- private FeatureFlags mFeatureFlags;
- @Mock
- private Optional<Bubbles> mOptionalBubbles;
- @Mock
- private Bubbles mBubbles;
+ @Mock private ServiceConnector<IAppClipsService> mServiceConnector;
@Mock
private NoteTaskController mNoteTaskController;
@Mock
private PackageManager mPackageManager;
@Mock
- private UserTracker mUserTracker;
- @Mock
private UiEventLogger mUiEventLogger;
@Mock
- private UserManager mUserManager;
-
+ private BroadcastSender mBroadcastSender;
+ @Background
+ private Executor mBgExecutor;
+ @Main
+ private Executor mMainExecutor;
@Main
private Handler mMainHandler;
@@ -114,9 +106,9 @@
new SingleActivityFactory<>(AppClipsTrampolineActivityTestable.class) {
@Override
protected AppClipsTrampolineActivityTestable create(Intent unUsed) {
- return new AppClipsTrampolineActivityTestable(mDevicePolicyManager,
- mFeatureFlags, mOptionalBubbles, mNoteTaskController, mPackageManager,
- mUserTracker, mUiEventLogger, mUserManager, mMainHandler);
+ return new AppClipsTrampolineActivityTestable(mServiceConnector,
+ mNoteTaskController, mPackageManager, mUiEventLogger, mBroadcastSender,
+ mBgExecutor, mMainExecutor, mMainHandler);
}
};
@@ -133,6 +125,8 @@
mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH));
MockitoAnnotations.initMocks(this);
+ mBgExecutor = MoreExecutors.directExecutor();
+ mMainExecutor = MoreExecutors.directExecutor();
mMainHandler = mContext.getMainThreadHandler();
mActivityIntent = new Intent(mContext, AppClipsTrampolineActivityTestable.class);
@@ -169,19 +163,9 @@
}
@Test
- public void flagOff_shouldFinishWithResultCancel() {
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false);
-
- mActivityRule.launchActivity(mActivityIntent);
-
- assertThat(mActivityRule.getActivityResult().getResultCode())
- .isEqualTo(Activity.RESULT_CANCELED);
- }
-
- @Test
- public void bubblesEmpty_shouldFinishWithFailed() {
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
- when(mOptionalBubbles.isEmpty()).thenReturn(true);
+ public void queryService_returnedFailed_shouldFinishWithFailed() {
+ when(mServiceConnector.postForResult(any()))
+ .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_FAILED));
mActivityRule.launchActivity(mActivityIntent);
@@ -189,14 +173,13 @@
assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
assertThat(getStatusCodeExtra(actualResult.getResultData()))
.isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+ assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
}
@Test
- public void taskIdNotAppBubble_shouldFinishWithWindowModeUnsupported() {
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
- when(mOptionalBubbles.isEmpty()).thenReturn(false);
- when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(false);
+ public void queryService_returnedWindowModeUnsupported_shouldFinishWithWindowModeUnsupported() {
+ when(mServiceConnector.postForResult(any()))
+ .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED));
mActivityRule.launchActivity(mActivityIntent);
@@ -204,15 +187,13 @@
assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
assertThat(getStatusCodeExtra(actualResult.getResultData()))
.isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
+ assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
}
@Test
- public void dpmScreenshotBlocked_shouldFinishWithBlockedByAdmin() {
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
- when(mOptionalBubbles.isEmpty()).thenReturn(false);
- when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true);
- when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+ public void queryService_returnedScreenshotBlocked_shouldFinishWithBlockedByAdmin() {
+ when(mServiceConnector.postForResult(any()))
+ .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN));
mActivityRule.launchActivity(mActivityIntent);
@@ -220,6 +201,7 @@
assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
assertThat(getStatusCodeExtra(actualResult.getResultData()))
.isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
+ assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
}
@Test
@@ -240,6 +222,7 @@
assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
assertThat(getStatusCodeExtra(actualResult.getResultData()))
.isEqualTo(CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
+ assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
}
@Test
@@ -261,6 +244,7 @@
assertThat(getStatusCodeExtra(actualResult.getResultData()))
.isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI);
+ assertThat(mActivityRule.getActivity().isFinishing()).isTrue();
}
@Test
@@ -274,48 +258,9 @@
verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, TEST_CALLING_PACKAGE);
}
- @Test
- public void startAppClipsActivity_throughWPUser_shouldStartMainUserActivity()
- throws NameNotFoundException {
- when(mUserManager.isManagedProfile()).thenReturn(true);
- when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM);
- mockToSatisfyAllPrerequisites();
-
- AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent);
- waitForIdleSync();
-
- Intent actualIntent = activity.mStartedIntent;
- assertThat(actualIntent.getComponent()).isEqualTo(
- new ComponentName(mContext, AppClipsTrampolineActivity.class));
- assertThat(actualIntent.getFlags()).isEqualTo(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
- assertThat(actualIntent.getBooleanExtra(EXTRA_USE_WP_USER, false)).isTrue();
- assertThat(activity.mStartingUser).isEqualTo(UserHandle.SYSTEM);
- }
-
- @Test
- public void startAppClipsActivity_throughWPUser_noMainUser_shouldFinishWithFailed()
- throws NameNotFoundException {
- when(mUserManager.isManagedProfile()).thenReturn(true);
- when(mUserManager.getMainUser()).thenReturn(null);
-
- mockToSatisfyAllPrerequisites();
-
- mActivityRule.launchActivity(mActivityIntent);
- waitForIdleSync();
-
- ActivityResult actualResult = mActivityRule.getActivityResult();
- assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
- assertThat(getStatusCodeExtra(actualResult.getResultData()))
- .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
- }
-
private void mockToSatisfyAllPrerequisites() throws NameNotFoundException {
- when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
- when(mOptionalBubbles.isEmpty()).thenReturn(false);
- when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true);
- when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
- when(mUserTracker.getUserProfiles()).thenReturn(List.of());
+ when(mServiceConnector.postForResult(any()))
+ .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_SUCCESS));
ApplicationInfo testApplicationInfo = new ApplicationInfo();
testApplicationInfo.uid = TEST_UID;
@@ -330,17 +275,14 @@
Intent mStartedIntent;
UserHandle mStartingUser;
- public AppClipsTrampolineActivityTestable(DevicePolicyManager devicePolicyManager,
- FeatureFlags flags,
- Optional<Bubbles> optionalBubbles,
- NoteTaskController noteTaskController,
- PackageManager packageManager,
- UserTracker userTracker,
- UiEventLogger uiEventLogger,
- UserManager userManager,
+ public AppClipsTrampolineActivityTestable(
+ ServiceConnector<IAppClipsService> serviceServiceConnector,
+ NoteTaskController noteTaskController, PackageManager packageManager,
+ UiEventLogger uiEventLogger, BroadcastSender broadcastSender,
+ @Background Executor bgExecutor, @Main Executor mainExecutor,
@Main Handler mainHandler) {
- super(devicePolicyManager, flags, optionalBubbles, noteTaskController, packageManager,
- userTracker, uiEventLogger, userManager, mainHandler);
+ super(serviceServiceConnector, noteTaskController, packageManager, uiEventLogger,
+ broadcastSender, bgExecutor, mainExecutor, mainHandler);
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 526dc8d..cde6ac0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -126,7 +126,7 @@
}
};
mNotificationShadeWindowController.setScrimsVisibilityListener((visibility) -> {});
- mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
+ mNotificationShadeWindowController.setWindowRootView(mNotificationShadeWindowView);
mNotificationShadeWindowController.attach();
verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 16277de..0393bef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -21,6 +21,7 @@
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardMessageAreaController
import com.android.keyguard.KeyguardSecurityContainerController
import com.android.keyguard.LockIconViewController
import com.android.keyguard.dagger.KeyguardBouncerComponent
@@ -29,14 +30,20 @@
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
+import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.keyguard.bouncer.domain.interactor.CountDownTimerUtil
+import com.android.systemui.keyguard.data.repository.FakeBouncerMessageRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.log.BouncerLogger
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.data.repository.MultiShadeRepository
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
@@ -54,6 +61,7 @@
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -104,7 +112,8 @@
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@Mock
- private lateinit var unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
+ private lateinit var unfoldTransitionProgressProvider:
+ Optional<UnfoldTransitionProgressProvider>
@Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Mock
lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
@@ -134,6 +143,7 @@
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
+ featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
@@ -170,6 +180,7 @@
pulsingGestureListener,
keyguardBouncerViewModel,
keyguardBouncerComponentFactory,
+ mock(KeyguardMessageAreaController.Factory::class.java),
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
@@ -189,6 +200,10 @@
shadeController = shadeController,
)
},
+ BouncerMessageInteractor(FakeBouncerMessageRepository(),
+ mock(BouncerMessageFactory::class.java),
+ FakeUserRepository(), CountDownTimerUtil(), featureFlags),
+ BouncerLogger(logcatLogBuffer("BouncerLog"))
)
underTest.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 16af208..5ae8ee1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -21,6 +21,7 @@
import android.view.MotionEvent
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardMessageAreaController
import com.android.keyguard.KeyguardSecurityContainerController
import com.android.keyguard.LockIconViewController
import com.android.keyguard.dagger.KeyguardBouncerComponent
@@ -29,13 +30,19 @@
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
+import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor
+import com.android.systemui.keyguard.bouncer.domain.interactor.CountDownTimerUtil
+import com.android.systemui.keyguard.data.repository.FakeBouncerMessageRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.log.BouncerLogger
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.data.repository.MultiShadeRepository
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
@@ -54,11 +61,13 @@
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.user.data.repository.FakeUserRepository
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.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
@@ -69,10 +78,10 @@
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-import java.util.Optional
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@@ -106,7 +115,8 @@
@Mock
private lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@Mock
- private lateinit var unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
+ private lateinit var unfoldTransitionProgressProvider:
+ Optional<UnfoldTransitionProgressProvider>
@Mock private lateinit var notificationInsetsController: NotificationInsetsController
@Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Mock
@@ -146,6 +156,7 @@
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
+ featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
val multiShadeInteractor =
@@ -181,6 +192,7 @@
pulsingGestureListener,
keyguardBouncerViewModel,
keyguardBouncerComponentFactory,
+ Mockito.mock(KeyguardMessageAreaController.Factory::class.java),
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
@@ -200,6 +212,14 @@
shadeController = shadeController,
)
},
+ BouncerMessageInteractor(
+ FakeBouncerMessageRepository(),
+ Mockito.mock(BouncerMessageFactory::class.java),
+ FakeUserRepository(),
+ CountDownTimerUtil(),
+ featureFlags
+ ),
+ BouncerLogger(logcatLogBuffer("BouncerLog"))
)
controller.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 542e0cb..c810f0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -102,6 +102,7 @@
import com.android.systemui.keyguard.KeyguardIndication;
import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController;
import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -297,7 +298,8 @@
mFaceHelpMessageDeferral, mock(KeyguardLogger.class),
mAlternateBouncerInteractor,
mAlarmManager,
- mUserTracker
+ mUserTracker,
+ mock(BouncerMessageInteractor.class)
);
mController.init();
mController.setIndicationArea(mIndicationArea);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index fd9f6a7..4ed113f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -287,7 +287,6 @@
@Mock private StatusBarTouchableRegionManager mStatusBarTouchableRegionManager;
@Mock private ScreenPinningRequest mScreenPinningRequest;
@Mock private PluginDependencyProvider mPluginDependencyProvider;
- @Mock private KeyguardDismissUtil mKeyguardDismissUtil;
@Mock private ExtensionController mExtensionController;
@Mock private UserInfoControllerImpl mUserInfoControllerImpl;
@Mock private PhoneStatusBarPolicy mPhoneStatusBarPolicy;
@@ -516,7 +515,6 @@
mInitController,
new Handler(TestableLooper.get(this).getLooper()),
mPluginDependencyProvider,
- mKeyguardDismissUtil,
mExtensionController,
mUserInfoControllerImpl,
mPhoneStatusBarPolicy,
@@ -1103,8 +1101,10 @@
clearInvocations(mNotificationPanelViewController);
mCentralSurfaces.mWakefulnessObserver.onStartedWakingUp();
- verify(mDozeServiceHost).stopDozing();
+ verify(mDozeServiceHost, never()).stopDozing();
verify(mNotificationPanelViewController).expand(eq(false));
+ mCentralSurfaces.mWakefulnessObserver.onFinishedWakingUp();
+ verify(mDozeServiceHost).stopDozing();
}
@Test
@@ -1118,6 +1118,8 @@
mCentralSurfaces.setBouncerShowing(true);
mCentralSurfaces.mWakefulnessObserver.onStartedWakingUp();
verify(mNotificationPanelViewController, never()).expand(anyBoolean());
+ mCentralSurfaces.mWakefulnessObserver.onFinishedWakingUp();
+ verify(mDozeServiceHost).stopDozing();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardDismissUtilTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardDismissUtilTest.java
new file mode 100644
index 0000000..b0aa2d3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardDismissUtilTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class KeyguardDismissUtilTest extends SysuiTestCase {
+ @Mock
+ private KeyguardStateController mKeyguardStateController;
+ @Mock
+ private SysuiStatusBarStateController mStatusBarStateController;
+ @Mock
+ private ActivityStarter mActivityStarter;
+ @Mock
+ private ActivityStarter.OnDismissAction mAction;
+
+ private KeyguardDismissUtil mKeyguardDismissUtil;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mKeyguardDismissUtil = new KeyguardDismissUtil(
+ mKeyguardStateController, mStatusBarStateController, mActivityStarter);
+ }
+
+ @Test
+ public void testSetLeaveOpenOnKeyguardHideWhenKeyGuardStateControllerIsShowing() {
+ doReturn(true).when(mKeyguardStateController).isShowing();
+
+ mKeyguardDismissUtil.executeWhenUnlocked(mAction, true /* requiresShadeOpen */,
+ true /* afterKeyguardGone */);
+
+ verify(mStatusBarStateController).setLeaveOpenOnKeyguardHide(true);
+
+ verify(mActivityStarter).dismissKeyguardThenExecute(mAction, null, true);
+
+ }
+
+ @Test
+ public void testSetLeaveOpenOnKeyguardHideWhenKeyGuardStateControllerIsNotShowing() {
+ doReturn(false).when(mKeyguardStateController).isShowing();
+
+ mKeyguardDismissUtil.executeWhenUnlocked(mAction, true /* requiresShadeOpen */,
+ true /* afterKeyguardGone */);
+
+ //no interaction with mStatusBarStateController
+ verify(mStatusBarStateController, times(0)).setLeaveOpenOnKeyguardHide(true);
+
+ verify(mActivityStarter).dismissKeyguardThenExecute(mAction, null, true);
+
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
index 3b0d512..ae38958e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
@@ -51,9 +51,11 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.SmartReplyController;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
@@ -107,14 +109,15 @@
private SmartReplyInflaterImpl mSmartReplyInflater;
private SmartActionInflaterImpl mSmartActionInflater;
+ private KeyguardDismissUtil mKeyguardDismissUtil;
@Mock private SmartReplyConstants mConstants;
@Mock private ActivityStarter mActivityStarter;
@Mock private HeadsUpManager mHeadsUpManager;
@Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
@Mock private SmartReplyController mSmartReplyController;
-
- private final KeyguardDismissUtil mKeyguardDismissUtil = new KeyguardDismissUtil();
+ @Mock private KeyguardStateController mKeyguardStateController;
+ @Mock private SysuiStatusBarStateController mStatusBarStateController;
@Before
public void setUp() {
@@ -122,12 +125,15 @@
mReceiver = new BlockingQueueIntentReceiver();
mContext.registerReceiver(mReceiver, new IntentFilter(TEST_ACTION),
Context.RECEIVER_EXPORTED_UNAUDITED);
- mKeyguardDismissUtil.setDismissHandler((action, unused, afterKgGone) -> action.onDismiss());
+
mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
mDependency.injectMockDependency(ShadeController.class);
mDependency.injectMockDependency(NotificationRemoteInputManager.class);
mDependency.injectTestDependency(ActivityStarter.class, mActivityStarter);
mDependency.injectTestDependency(SmartReplyConstants.class, mConstants);
+ mDependency.injectTestDependency(KeyguardStateController.class, mKeyguardStateController);
+ mDependency.injectTestDependency(StatusBarStateController.class, mStatusBarStateController);
+
// Any number of replies are fine.
when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(0);
@@ -153,6 +159,13 @@
mActionIcon = Icon.createWithResource(mContext, R.drawable.ic_person);
+ mKeyguardDismissUtil = new KeyguardDismissUtil(
+ mKeyguardStateController, mStatusBarStateController, mActivityStarter) {
+ public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
+ boolean requiresShadeOpen, boolean afterKeyguardGone) {
+ action.onDismiss();
+ }
+ };
mSmartReplyInflater = new SmartReplyInflaterImpl(
mConstants,
mKeyguardDismissUtil,
@@ -185,7 +198,17 @@
@Test
public void testSendSmartReply_keyguardCancelled() throws InterruptedException {
- mKeyguardDismissUtil.setDismissHandler((action, unused, afterKgGone) -> { });
+ mKeyguardDismissUtil = new KeyguardDismissUtil(
+ mKeyguardStateController, mStatusBarStateController, mActivityStarter) {
+ public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
+ boolean requiresShadeOpen, boolean afterKeyguardGone) { }};
+ mSmartReplyInflater = new SmartReplyInflaterImpl(
+ mConstants,
+ mKeyguardDismissUtil,
+ mNotificationRemoteInputManager,
+ mSmartReplyController,
+ mContext);
+
setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
@@ -196,9 +219,20 @@
@Test
public void testSendSmartReply_waitsForKeyguard() throws InterruptedException {
AtomicReference<OnDismissAction> actionRef = new AtomicReference<>();
+ mKeyguardDismissUtil = new KeyguardDismissUtil(
+ mKeyguardStateController, mStatusBarStateController, mActivityStarter) {
+ public void executeWhenUnlocked(ActivityStarter.OnDismissAction action,
+ boolean requiresShadeOpen, boolean afterKeyguardGone) {
+ actionRef.set(action);
+ }
+ };
+ mSmartReplyInflater = new SmartReplyInflaterImpl(
+ mConstants,
+ mKeyguardDismissUtil,
+ mNotificationRemoteInputManager,
+ mSmartReplyController,
+ mContext);
- mKeyguardDismissUtil.setDismissHandler((action, unused, afterKgGone)
- -> actionRef.set(action));
setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 47a86b1..2158396 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -320,7 +320,7 @@
mColorExtractor, mDumpManager, mKeyguardStateController,
mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager,
mShadeWindowLogger);
- mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
+ mNotificationShadeWindowController.setWindowRootView(mNotificationShadeWindowView);
mNotificationShadeWindowController.attach();
mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 995e557..d5aee92 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -2859,8 +2859,7 @@
if (DEBUG) {
Slog.d(
TAG,
- addUserIdToLogMessage(
- mUserId, "Starting backup confirmation UI, token=" + token));
+ addUserIdToLogMessage(mUserId, "Starting backup confirmation UI"));
}
if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) {
Slog.e(
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 61fc32d..51359ad 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -163,13 +163,35 @@
private boolean mDisabledByDeviceConfig;
// Device-config settings that are cached and passed back to apps
- @GuardedBy("mLock") int mDevCfgLoggingLevel;
- @GuardedBy("mLock") int mDevCfgMaxBufferSize;
- @GuardedBy("mLock") int mDevCfgIdleFlushingFrequencyMs;
- @GuardedBy("mLock") int mDevCfgTextChangeFlushingFrequencyMs;
- @GuardedBy("mLock") int mDevCfgLogHistorySize;
- @GuardedBy("mLock") int mDevCfgIdleUnbindTimeoutMs;
- @GuardedBy("mLock") boolean mDevCfgDisableFlushForViewTreeAppearing;
+ @GuardedBy("mLock")
+ int mDevCfgLoggingLevel;
+
+ @GuardedBy("mLock")
+ int mDevCfgMaxBufferSize;
+
+ @GuardedBy("mLock")
+ int mDevCfgIdleFlushingFrequencyMs;
+
+ @GuardedBy("mLock")
+ int mDevCfgTextChangeFlushingFrequencyMs;
+
+ @GuardedBy("mLock")
+ int mDevCfgLogHistorySize;
+
+ @GuardedBy("mLock")
+ int mDevCfgIdleUnbindTimeoutMs;
+
+ @GuardedBy("mLock")
+ boolean mDevCfgDisableFlushForViewTreeAppearing;
+
+ @GuardedBy("mLock")
+ boolean mDevCfgEnableContentProtectionReceiver;
+
+ @GuardedBy("mLock")
+ int mDevCfgContentProtectionAppsBlocklistSize;
+
+ @GuardedBy("mLock")
+ int mDevCfgContentProtectionBufferSize;
private final Executor mDataShareExecutor = Executors.newCachedThreadPool();
private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -362,6 +384,11 @@
case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT:
case ContentCaptureManager
.DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING:
+ case ContentCaptureManager
+ .DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER:
+ case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE:
+ case ContentCaptureManager
+ .DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE:
setFineTuneParamsFromDeviceConfig();
return;
default:
@@ -372,39 +399,78 @@
private void setFineTuneParamsFromDeviceConfig() {
synchronized (mLock) {
- mDevCfgMaxBufferSize = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
- ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
- ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE);
- mDevCfgIdleFlushingFrequencyMs = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
- ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY,
- ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS);
- mDevCfgTextChangeFlushingFrequencyMs = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
- ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY,
- ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS);
- mDevCfgLogHistorySize = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
- ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, 20);
- mDevCfgIdleUnbindTimeoutMs = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
- ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT,
- (int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
- mDevCfgDisableFlushForViewTreeAppearing = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
- ContentCaptureManager
- .DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
- false);
+ mDevCfgMaxBufferSize =
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
+ ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE);
+ mDevCfgIdleFlushingFrequencyMs =
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY,
+ ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS);
+ mDevCfgTextChangeFlushingFrequencyMs =
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager
+ .DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY,
+ ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS);
+ mDevCfgLogHistorySize =
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE,
+ 20);
+ mDevCfgIdleUnbindTimeoutMs =
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT,
+ (int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
+ mDevCfgDisableFlushForViewTreeAppearing =
+ DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager
+ .DEVICE_CONFIG_PROPERTY_DISABLE_FLUSH_FOR_VIEW_TREE_APPEARING,
+ false);
+ mDevCfgEnableContentProtectionReceiver =
+ DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager
+ .DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER,
+ ContentCaptureManager.DEFAULT_ENABLE_CONTENT_PROTECTION_RECEIVER);
+ mDevCfgContentProtectionAppsBlocklistSize =
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager
+ .DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE,
+ ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_APPS_BLOCKLIST_SIZE);
+ mDevCfgContentProtectionBufferSize =
+ DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ ContentCaptureManager
+ .DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_BUFFER_SIZE,
+ ContentCaptureManager.DEFAULT_CONTENT_PROTECTION_BUFFER_SIZE);
if (verbose) {
- Slog.v(TAG, "setFineTuneParamsFromDeviceConfig(): "
- + "bufferSize=" + mDevCfgMaxBufferSize
- + ", idleFlush=" + mDevCfgIdleFlushingFrequencyMs
- + ", textFluxh=" + mDevCfgTextChangeFlushingFrequencyMs
- + ", logHistory=" + mDevCfgLogHistorySize
- + ", idleUnbindTimeoutMs=" + mDevCfgIdleUnbindTimeoutMs
- + ", disableFlushForViewTreeAppearing="
- + mDevCfgDisableFlushForViewTreeAppearing);
+ Slog.v(
+ TAG,
+ "setFineTuneParamsFromDeviceConfig(): "
+ + "bufferSize="
+ + mDevCfgMaxBufferSize
+ + ", idleFlush="
+ + mDevCfgIdleFlushingFrequencyMs
+ + ", textFluxh="
+ + mDevCfgTextChangeFlushingFrequencyMs
+ + ", logHistory="
+ + mDevCfgLogHistorySize
+ + ", idleUnbindTimeoutMs="
+ + mDevCfgIdleUnbindTimeoutMs
+ + ", disableFlushForViewTreeAppearing="
+ + mDevCfgDisableFlushForViewTreeAppearing
+ + ", enableContentProtectionReceiver="
+ + mDevCfgEnableContentProtectionReceiver
+ + ", contentProtectionAppsBlocklistSize="
+ + mDevCfgContentProtectionAppsBlocklistSize
+ + ", contentProtectionBufferSize="
+ + mDevCfgContentProtectionBufferSize);
}
}
}
@@ -645,21 +711,46 @@
final String prefix2 = prefix + " ";
- pw.print(prefix); pw.print("Users disabled by Settings: "); pw.println(mDisabledBySettings);
- pw.print(prefix); pw.println("DeviceConfig Settings: ");
- pw.print(prefix2); pw.print("disabled: "); pw.println(mDisabledByDeviceConfig);
- pw.print(prefix2); pw.print("loggingLevel: "); pw.println(mDevCfgLoggingLevel);
- pw.print(prefix2); pw.print("maxBufferSize: "); pw.println(mDevCfgMaxBufferSize);
- pw.print(prefix2); pw.print("idleFlushingFrequencyMs: ");
+ pw.print(prefix);
+ pw.print("Users disabled by Settings: ");
+ pw.println(mDisabledBySettings);
+ pw.print(prefix);
+ pw.println("DeviceConfig Settings: ");
+ pw.print(prefix2);
+ pw.print("disabled: ");
+ pw.println(mDisabledByDeviceConfig);
+ pw.print(prefix2);
+ pw.print("loggingLevel: ");
+ pw.println(mDevCfgLoggingLevel);
+ pw.print(prefix2);
+ pw.print("maxBufferSize: ");
+ pw.println(mDevCfgMaxBufferSize);
+ pw.print(prefix2);
+ pw.print("idleFlushingFrequencyMs: ");
pw.println(mDevCfgIdleFlushingFrequencyMs);
- pw.print(prefix2); pw.print("textChangeFlushingFrequencyMs: ");
+ pw.print(prefix2);
+ pw.print("textChangeFlushingFrequencyMs: ");
pw.println(mDevCfgTextChangeFlushingFrequencyMs);
- pw.print(prefix2); pw.print("logHistorySize: "); pw.println(mDevCfgLogHistorySize);
- pw.print(prefix2); pw.print("idleUnbindTimeoutMs: ");
+ pw.print(prefix2);
+ pw.print("logHistorySize: ");
+ pw.println(mDevCfgLogHistorySize);
+ pw.print(prefix2);
+ pw.print("idleUnbindTimeoutMs: ");
pw.println(mDevCfgIdleUnbindTimeoutMs);
- pw.print(prefix2); pw.print("disableFlushForViewTreeAppearing: ");
+ pw.print(prefix2);
+ pw.print("disableFlushForViewTreeAppearing: ");
pw.println(mDevCfgDisableFlushForViewTreeAppearing);
- pw.print(prefix); pw.println("Global Options:");
+ pw.print(prefix2);
+ pw.print("enableContentProtectionReceiver: ");
+ pw.println(mDevCfgEnableContentProtectionReceiver);
+ pw.print(prefix2);
+ pw.print("contentProtectionAppsBlocklistSize: ");
+ pw.println(mDevCfgContentProtectionAppsBlocklistSize);
+ pw.print(prefix2);
+ pw.print("contentProtectionBufferSize: ");
+ pw.println(mDevCfgContentProtectionBufferSize);
+ pw.print(prefix);
+ pw.println("Global Options:");
mGlobalContentCaptureOptions.dump(prefix2, pw);
}
@@ -1019,11 +1110,19 @@
}
synchronized (mLock) {
- final ContentCaptureOptions options = new ContentCaptureOptions(mDevCfgLoggingLevel,
- mDevCfgMaxBufferSize, mDevCfgIdleFlushingFrequencyMs,
- mDevCfgTextChangeFlushingFrequencyMs, mDevCfgLogHistorySize,
- mDevCfgDisableFlushForViewTreeAppearing,
- whitelistedComponents);
+ final ContentCaptureOptions options =
+ new ContentCaptureOptions(
+ mDevCfgLoggingLevel,
+ mDevCfgMaxBufferSize,
+ mDevCfgIdleFlushingFrequencyMs,
+ mDevCfgTextChangeFlushingFrequencyMs,
+ mDevCfgLogHistorySize,
+ mDevCfgDisableFlushForViewTreeAppearing,
+ /* enableReceiver= */ true,
+ new ContentCaptureOptions.ContentProtectionOptions(
+ mDevCfgEnableContentProtectionReceiver,
+ mDevCfgContentProtectionBufferSize),
+ whitelistedComponents);
if (verbose) Slog.v(TAG, "getOptionsForPackage(" + packageName + "): " + options);
return options;
}
diff --git a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java
new file mode 100644
index 0000000..715cf9a
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionBlocklistManager.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.contentprotection;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.PackageInfo;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Manages whether the content protection is enabled for an app using a blocklist.
+ *
+ * @hide
+ */
+class ContentProtectionBlocklistManager {
+
+ private static final String TAG = "ContentProtectionBlocklistManager";
+
+ private static final String PACKAGE_NAME_BLOCKLIST_FILENAME =
+ "/product/etc/res/raw/content_protection/package_name_blocklist.txt";
+
+ @NonNull private final ContentProtectionPackageManager mContentProtectionPackageManager;
+
+ @Nullable private Set<String> mPackageNameBlocklist;
+
+ protected ContentProtectionBlocklistManager(
+ @NonNull ContentProtectionPackageManager contentProtectionPackageManager) {
+ mContentProtectionPackageManager = contentProtectionPackageManager;
+ }
+
+ public boolean isAllowed(@NonNull String packageName) {
+ if (mPackageNameBlocklist == null) {
+ // List not loaded or failed to load, don't run on anything
+ return false;
+ }
+ if (mPackageNameBlocklist.contains(packageName)) {
+ return false;
+ }
+ PackageInfo packageInfo = mContentProtectionPackageManager.getPackageInfo(packageName);
+ if (packageInfo == null) {
+ return false;
+ }
+ if (!mContentProtectionPackageManager.hasRequestedInternetPermissions(packageInfo)) {
+ return false;
+ }
+ if (mContentProtectionPackageManager.isSystemApp(packageInfo)) {
+ return false;
+ }
+ if (mContentProtectionPackageManager.isUpdatedSystemApp(packageInfo)) {
+ return false;
+ }
+ return true;
+ }
+
+ public void updateBlocklist(int blocklistSize) {
+ Slog.i(TAG, "Blocklist size updating to: " + blocklistSize);
+ mPackageNameBlocklist = readPackageNameBlocklist(blocklistSize);
+ }
+
+ @Nullable
+ private Set<String> readPackageNameBlocklist(int blocklistSize) {
+ if (blocklistSize <= 0) {
+ // Explicitly requested an empty blocklist
+ return Collections.emptySet();
+ }
+ List<String> lines = readLinesFromRawFile(PACKAGE_NAME_BLOCKLIST_FILENAME);
+ if (lines == null) {
+ return null;
+ }
+ return lines.stream().limit(blocklistSize).collect(Collectors.toSet());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ protected List<String> readLinesFromRawFile(@NonNull String filename) {
+ try (FileReader fileReader = new FileReader(filename);
+ BufferedReader bufferedReader = new BufferedReader(fileReader)) {
+ return bufferedReader
+ .lines()
+ .map(line -> line.trim())
+ .filter(line -> !line.isBlank())
+ .collect(Collectors.toList());
+ } catch (Exception ex) {
+ Slog.e(TAG, "Failed to read: " + filename, ex);
+ return null;
+ }
+ }
+}
diff --git a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java
new file mode 100644
index 0000000..1847e5d
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionPackageManager.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.contentprotection;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManager.PackageInfoFlags;
+import android.util.Slog;
+
+import java.util.Arrays;
+
+/**
+ * Basic package manager for content protection using content capture.
+ *
+ * @hide
+ */
+public class ContentProtectionPackageManager {
+
+ private static final String TAG = "ContentProtectionPackageManager";
+
+ private static final PackageInfoFlags PACKAGE_INFO_FLAGS =
+ PackageInfoFlags.of(PackageManager.GET_PERMISSIONS);
+
+ @NonNull private final PackageManager mPackageManager;
+
+ ContentProtectionPackageManager(@NonNull Context context) {
+ mPackageManager = context.getPackageManager();
+ }
+
+ @Nullable
+ public PackageInfo getPackageInfo(@NonNull String packageName) {
+ try {
+ return mPackageManager.getPackageInfo(packageName, PACKAGE_INFO_FLAGS);
+ } catch (NameNotFoundException ex) {
+ Slog.w(TAG, "Package info not found for: " + packageName, ex);
+ return null;
+ }
+ }
+
+ public boolean isSystemApp(@NonNull PackageInfo packageInfo) {
+ return packageInfo.applicationInfo != null && isSystemApp(packageInfo.applicationInfo);
+ }
+
+ private boolean isSystemApp(@NonNull ApplicationInfo applicationInfo) {
+ return (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+ }
+
+ public boolean isUpdatedSystemApp(@NonNull PackageInfo packageInfo) {
+ return packageInfo.applicationInfo != null
+ && isUpdatedSystemApp(packageInfo.applicationInfo);
+ }
+
+ private boolean isUpdatedSystemApp(@NonNull ApplicationInfo applicationInfo) {
+ return (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
+ }
+
+ public boolean hasRequestedInternetPermissions(@NonNull PackageInfo packageInfo) {
+ return packageInfo.requestedPermissions != null
+ && Arrays.asList(packageInfo.requestedPermissions)
+ .contains(Manifest.permission.INTERNET);
+ }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index 05ca6e4..6ac1631 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -266,8 +266,12 @@
}
} else {
if (isBackgroundAuth) {
- Slog.e(TAG, "cancelling due to background auth");
- cancel();
+ Slog.e(TAG, "Sending cancel to client(Due to background auth)");
+ if (mTaskStackListener != null) {
+ mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
+ }
+ sendCancelOnly(getListener());
+ mCallback.onClientFinished(this, false);
} else {
// Allow system-defined limit of number of attempts before giving up
if (mShouldUseLockoutTracker) {
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 75709fbb..d647757 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -223,11 +223,11 @@
private final ShortTermModel mShortTermModel;
private final ShortTermModel mPausedShortTermModel;
- // Controls High Brightness Mode.
- private HighBrightnessModeController mHbmController;
+ // Controls Brightness range (including High Brightness Mode).
+ private final BrightnessRangeController mBrightnessRangeController;
// Throttles (caps) maximum allowed brightness
- private BrightnessThrottler mBrightnessThrottler;
+ private final BrightnessThrottler mBrightnessThrottler;
private boolean mIsBrightnessThrottled;
// Context-sensitive brightness configurations require keeping track of the foreground app's
@@ -257,7 +257,8 @@
HysteresisLevels screenBrightnessThresholds,
HysteresisLevels ambientBrightnessThresholdsIdle,
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
- HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
+ BrightnessRangeController brightnessModeController,
+ BrightnessThrottler brightnessThrottler,
BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
int ambientLightHorizonLong, float userLux, float userBrightness) {
this(new Injector(), callbacks, looper, sensorManager, lightSensor,
@@ -267,7 +268,7 @@
darkeningLightDebounceConfig, resetAmbientLuxAfterWarmUpConfig,
ambientBrightnessThresholds, screenBrightnessThresholds,
ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, context,
- hbmController, brightnessThrottler, idleModeBrightnessMapper,
+ brightnessModeController, brightnessThrottler, idleModeBrightnessMapper,
ambientLightHorizonShort, ambientLightHorizonLong, userLux, userBrightness
);
}
@@ -283,7 +284,8 @@
HysteresisLevels screenBrightnessThresholds,
HysteresisLevels ambientBrightnessThresholdsIdle,
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
- HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
+ BrightnessRangeController brightnessModeController,
+ BrightnessThrottler brightnessThrottler,
BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
int ambientLightHorizonLong, float userLux, float userBrightness) {
mInjector = injector;
@@ -326,7 +328,7 @@
mPendingForegroundAppPackageName = null;
mForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
mPendingForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
- mHbmController = hbmController;
+ mBrightnessRangeController = brightnessModeController;
mBrightnessThrottler = brightnessThrottler;
mInteractiveModeBrightnessMapper = interactiveModeBrightnessMapper;
mIdleModeBrightnessMapper = idleModeBrightnessMapper;
@@ -607,10 +609,11 @@
pw.println();
pw.println(" mInteractiveMapper=");
- mInteractiveModeBrightnessMapper.dump(pw, mHbmController.getNormalBrightnessMax());
+ mInteractiveModeBrightnessMapper.dump(pw,
+ mBrightnessRangeController.getNormalBrightnessMax());
if (mIdleModeBrightnessMapper != null) {
pw.println(" mIdleMapper=");
- mIdleModeBrightnessMapper.dump(pw, mHbmController.getNormalBrightnessMax());
+ mIdleModeBrightnessMapper.dump(pw, mBrightnessRangeController.getNormalBrightnessMax());
}
pw.println();
@@ -736,7 +739,7 @@
mAmbientDarkeningThreshold =
mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
}
- mHbmController.onAmbientLuxChange(mAmbientLux);
+ mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);
// If the short term model was invalidated and the change is drastic enough, reset it.
@@ -976,9 +979,9 @@
// Clamps values with float range [0.0-1.0]
private float clampScreenBrightness(float value) {
- final float minBrightness = Math.min(mHbmController.getCurrentBrightnessMin(),
+ final float minBrightness = Math.min(mBrightnessRangeController.getCurrentBrightnessMin(),
mBrightnessThrottler.getBrightnessCap());
- final float maxBrightness = Math.min(mHbmController.getCurrentBrightnessMax(),
+ final float maxBrightness = Math.min(mBrightnessRangeController.getCurrentBrightnessMax(),
mBrightnessThrottler.getBrightnessCap());
return MathUtils.constrain(value, minBrightness, maxBrightness);
}
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
new file mode 100644
index 0000000..12813c8
--- /dev/null
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import android.hardware.display.BrightnessInfo;
+import android.os.IBinder;
+
+import java.io.PrintWriter;
+
+class BrightnessRangeController {
+
+ private static final boolean NBM_FEATURE_FLAG = false;
+
+ private final HighBrightnessModeController mHbmController;
+ private final NormalBrightnessModeController mNormalBrightnessModeController =
+ new NormalBrightnessModeController();
+
+ private final Runnable mModeChangeCallback;
+
+ BrightnessRangeController(HighBrightnessModeController hbmController,
+ Runnable modeChangeCallback) {
+ mHbmController = hbmController;
+ mModeChangeCallback = modeChangeCallback;
+ }
+
+
+ void dump(PrintWriter pw) {
+ mHbmController.dump(pw);
+ }
+
+ void onAmbientLuxChange(float ambientLux) {
+ if (NBM_FEATURE_FLAG) {
+ boolean nbmTransitionChanged = mNormalBrightnessModeController.onAmbientLuxChange(
+ ambientLux);
+ int previousHbm = mHbmController.getHighBrightnessMode();
+ mHbmController.onAmbientLuxChange(ambientLux);
+ int nextHbm = mHbmController.getHighBrightnessMode();
+ // if hbm changed - callback was triggered in mHbmController.onAmbientLuxChange
+ // if nbm transition not changed - no need to trigger callback
+ if (previousHbm == nextHbm && nbmTransitionChanged) {
+ mModeChangeCallback.run();
+ }
+ } else {
+ mHbmController.onAmbientLuxChange(ambientLux);
+ }
+ }
+
+ float getNormalBrightnessMax() {
+ return mHbmController.getNormalBrightnessMax();
+ }
+
+ void loadFromConfig(HighBrightnessModeMetadata hbmMetadata, IBinder token,
+ DisplayDeviceInfo info, DisplayDeviceConfig displayDeviceConfig) {
+ mHbmController.setHighBrightnessModeMetadata(hbmMetadata);
+ mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId,
+ displayDeviceConfig.getHighBrightnessModeData(),
+ displayDeviceConfig::getHdrBrightnessFromSdr);
+ }
+
+ void stop() {
+ mHbmController.stop();
+ }
+
+ void setAutoBrightnessEnabled(int state) {
+ mHbmController.setAutoBrightnessEnabled(state);
+ }
+
+ void onBrightnessChanged(float brightness, float unthrottledBrightness,
+ @BrightnessInfo.BrightnessMaxReason int throttlingReason) {
+ mHbmController.onBrightnessChanged(brightness, unthrottledBrightness, throttlingReason);
+ }
+
+ float getCurrentBrightnessMin() {
+ return mHbmController.getCurrentBrightnessMin();
+ }
+
+
+ float getCurrentBrightnessMax() {
+ if (NBM_FEATURE_FLAG && mHbmController.getHighBrightnessMode()
+ == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF) {
+ return Math.min(mHbmController.getCurrentBrightnessMax(),
+ mNormalBrightnessModeController.getCurrentBrightnessMax());
+ }
+ return mHbmController.getCurrentBrightnessMax();
+ }
+
+ int getHighBrightnessMode() {
+ return mHbmController.getHighBrightnessMode();
+ }
+
+ float getHdrBrightnessValue() {
+ return mHbmController.getHdrBrightnessValue();
+ }
+
+ float getTransitionPoint() {
+ return mHbmController.getTransitionPoint();
+ }
+}
diff --git a/services/core/java/com/android/server/display/BrightnessSetting.java b/services/core/java/com/android/server/display/BrightnessSetting.java
index de42370..651828b 100644
--- a/services/core/java/com/android/server/display/BrightnessSetting.java
+++ b/services/core/java/com/android/server/display/BrightnessSetting.java
@@ -40,6 +40,7 @@
private final LogicalDisplay mLogicalDisplay;
+ private int mUserSerial;
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
@@ -56,13 +57,15 @@
@GuardedBy("mSyncRoot")
private float mBrightness;
- BrightnessSetting(@NonNull PersistentDataStore persistentDataStore,
+ BrightnessSetting(int userSerial,
+ @NonNull PersistentDataStore persistentDataStore,
@NonNull LogicalDisplay logicalDisplay,
DisplayManagerService.SyncRoot syncRoot) {
mPersistentDataStore = persistentDataStore;
mLogicalDisplay = logicalDisplay;
+ mUserSerial = userSerial;
mBrightness = mPersistentDataStore.getBrightness(
- mLogicalDisplay.getPrimaryDisplayDeviceLocked());
+ mLogicalDisplay.getPrimaryDisplayDeviceLocked(), userSerial);
mSyncRoot = syncRoot;
}
@@ -96,8 +99,13 @@
mListeners.remove(l);
}
+ /** Sets the user serial for the brightness setting */
+ public void setUserSerial(int userSerial) {
+ mUserSerial = userSerial;
+ }
+
/**
- * Sets the brigtness and broadcasts the change to the listeners.
+ * Sets the brightness and broadcasts the change to the listeners.
* @param brightness The value to which the brightness is to be set.
*/
public void setBrightness(float brightness) {
@@ -112,7 +120,8 @@
// changed.
if (brightness != mBrightness) {
mPersistentDataStore.setBrightness(mLogicalDisplay.getPrimaryDisplayDeviceLocked(),
- brightness);
+ brightness, mUserSerial
+ );
}
mBrightness = brightness;
int toSend = Float.floatToIntBits(mBrightness);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3844529..be65c53 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -652,6 +652,12 @@
logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(),
userSerial);
dpc.setBrightnessConfiguration(config, /* shouldResetShortTermModel= */ true);
+ // change the brightness value according to the selected user.
+ final DisplayDevice device = logicalDisplay.getPrimaryDisplayDeviceLocked();
+ if (device != null) {
+ dpc.setBrightness(
+ mPersistentDataStore.getBrightness(device, userSerial), userSerial);
+ }
}
dpc.onSwitchUser(newUserId);
});
@@ -3134,8 +3140,9 @@
mBrightnessTracker = new BrightnessTracker(mContext, null);
}
- final BrightnessSetting brightnessSetting = new BrightnessSetting(mPersistentDataStore,
- display, mSyncRoot);
+ final int userSerial = getUserManager().getUserSerialNumber(mContext.getUserId());
+ final BrightnessSetting brightnessSetting = new BrightnessSetting(userSerial,
+ mPersistentDataStore, display, mSyncRoot);
final DisplayPowerControllerInterface displayPowerController;
// If display is internal and has a HighBrightnessModeMetadata mapping, use that.
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 9d31572..7d8bde9 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -445,7 +445,7 @@
private final ColorDisplayServiceInternal mCdsi;
private float[] mNitsRange;
- private final HighBrightnessModeController mHbmController;
+ private final BrightnessRangeController mBrightnessRangeController;
private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
private final BrightnessThrottler mBrightnessThrottler;
@@ -654,8 +654,19 @@
loadBrightnessRampRates();
mSkipScreenOnBrightnessRamp = resources.getBoolean(
com.android.internal.R.bool.config_skipScreenOnBrightnessRamp);
+ Runnable modeChangeCallback = () -> {
+ sendUpdatePowerState();
+ postBrightnessChangeRunnable();
+ // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern.
+ if (mAutomaticBrightnessController != null) {
+ mAutomaticBrightnessController.update();
+ }
+ };
- mHbmController = createHbmControllerLocked();
+ HighBrightnessModeController hbmController = createHbmControllerLocked(modeChangeCallback);
+
+ mBrightnessRangeController = new BrightnessRangeController(hbmController,
+ modeChangeCallback);
mBrightnessThrottler = createBrightnessThrottlerLocked();
@@ -802,7 +813,7 @@
@Override
public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux) {
- mHbmController.onAmbientLuxChange(ambientLux);
+ mBrightnessRangeController.onAmbientLuxChange(ambientLux);
if (nits < 0) {
mBrightnessToFollow = leadDisplayBrightness;
} else {
@@ -1039,17 +1050,7 @@
mBrightnessRampIncreaseMaxTimeMillis,
mBrightnessRampDecreaseMaxTimeMillis);
}
- mHbmController.setHighBrightnessModeMetadata(hbmMetadata);
- mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId,
- mDisplayDeviceConfig.getHighBrightnessModeData(),
- new HighBrightnessModeController.HdrBrightnessDeviceConfig() {
- @Override
- public float getHdrBrightnessFromSdr(
- float sdrBrightness, float maxDesiredHdrSdrRatio) {
- return mDisplayDeviceConfig.getHdrBrightnessFromSdr(
- sdrBrightness, maxDesiredHdrSdrRatio);
- }
- });
+ mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);
mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(),
mThermalBrightnessThrottlingDataId, mUniqueDisplayId);
@@ -1264,7 +1265,7 @@
darkeningLightDebounce, autoBrightnessResetAmbientLuxAfterWarmUp,
ambientBrightnessThresholds, screenBrightnessThresholds,
ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext,
- mHbmController, mBrightnessThrottler, mIdleModeBrightnessMapper,
+ mBrightnessRangeController, mBrightnessThrottler, mIdleModeBrightnessMapper,
mDisplayDeviceConfig.getAmbientHorizonShort(),
mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userBrightness);
@@ -1364,7 +1365,7 @@
/** Clean up all resources that are accessed via the {@link #mHandler} thread. */
private void cleanupHandlerThreadAfterStop() {
setProximitySensorEnabled(false);
- mHbmController.stop();
+ mBrightnessRangeController.stop();
mBrightnessThrottler.stop();
mHandler.removeCallbacksAndMessages(null);
@@ -1647,7 +1648,7 @@
mShouldResetShortTermModel);
mShouldResetShortTermModel = false;
}
- mHbmController.setAutoBrightnessEnabled(mUseAutoBrightness
+ mBrightnessRangeController.setAutoBrightnessEnabled(mUseAutoBrightness
? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
: AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
@@ -1820,7 +1821,7 @@
// here instead of having HbmController listen to the brightness setting because certain
// brightness sources (such as an app override) are not saved to the setting, but should be
// reflected in HBM calculations.
- mHbmController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
+ mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
mBrightnessThrottler.getBrightnessMaxReason());
// Animate the screen brightness when the screen is on or dozing.
@@ -1874,13 +1875,14 @@
float sdrAnimateValue = animateValue;
// TODO(b/216365040): The decision to prevent HBM for HDR in low power mode should be
// done in HighBrightnessModeController.
- if (mHbmController.getHighBrightnessMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR
+ if (mBrightnessRangeController.getHighBrightnessMode()
+ == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR
&& (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_DIMMED) == 0
&& (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_LOW_POWER)
== 0) {
// We want to scale HDR brightness level with the SDR level, we also need to restore
// SDR brightness immediately when entering dim or low power mode.
- animateValue = mHbmController.getHdrBrightnessValue();
+ animateValue = mBrightnessRangeController.getHdrBrightnessValue();
}
final float currentBrightness = mPowerState.getScreenBrightness();
@@ -1942,8 +1944,8 @@
mTempBrightnessEvent.setBrightness(brightnessState);
mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId);
mTempBrightnessEvent.setReason(mBrightnessReason);
- mTempBrightnessEvent.setHbmMax(mHbmController.getCurrentBrightnessMax());
- mTempBrightnessEvent.setHbmMode(mHbmController.getHighBrightnessMode());
+ mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax());
+ mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());
mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
| (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
| (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0));
@@ -2104,9 +2106,11 @@
private boolean saveBrightnessInfo(float brightness, float adjustedBrightness) {
synchronized (mCachedBrightnessInfo) {
- final float minBrightness = Math.min(mHbmController.getCurrentBrightnessMin(),
+ final float minBrightness = Math.min(
+ mBrightnessRangeController.getCurrentBrightnessMin(),
mBrightnessThrottler.getBrightnessCap());
- final float maxBrightness = Math.min(mHbmController.getCurrentBrightnessMax(),
+ final float maxBrightness = Math.min(
+ mBrightnessRangeController.getCurrentBrightnessMax(),
mBrightnessThrottler.getBrightnessCap());
boolean changed = false;
@@ -2124,10 +2128,10 @@
maxBrightness);
changed |=
mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.hbmMode,
- mHbmController.getHighBrightnessMode());
+ mBrightnessRangeController.getHighBrightnessMode());
changed |=
mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.hbmTransitionPoint,
- mHbmController.getTransitionPoint());
+ mBrightnessRangeController.getTransitionPoint());
changed |=
mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,
mBrightnessThrottler.getBrightnessMaxReason());
@@ -2140,7 +2144,8 @@
mHandler.post(mOnBrightnessChangeRunnable);
}
- private HighBrightnessModeController createHbmControllerLocked() {
+ private HighBrightnessModeController createHbmControllerLocked(
+ Runnable modeChangeCallback) {
final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
final IBinder displayToken =
@@ -2159,15 +2164,7 @@
return mDisplayDeviceConfig.getHdrBrightnessFromSdr(
sdrBrightness, maxDesiredHdrSdrRatio);
}
- },
- () -> {
- sendUpdatePowerState();
- postBrightnessChangeRunnable();
- // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern.
- if (mAutomaticBrightnessController != null) {
- mAutomaticBrightnessController.update();
- }
- }, mHighBrightnessModeMetadata, mContext);
+ }, modeChangeCallback, mHighBrightnessModeMetadata, mContext);
}
private BrightnessThrottler createBrightnessThrottlerLocked() {
@@ -2328,8 +2325,8 @@
if (Float.isNaN(value)) {
value = PowerManager.BRIGHTNESS_MIN;
}
- return MathUtils.constrain(value,
- mHbmController.getCurrentBrightnessMin(), mHbmController.getCurrentBrightnessMax());
+ return MathUtils.constrain(value, mBrightnessRangeController.getCurrentBrightnessMin(),
+ mBrightnessRangeController.getCurrentBrightnessMax());
}
// Checks whether the brightness is within the valid brightness range, not including off.
@@ -2667,9 +2664,10 @@
}
@Override
- public void setBrightness(float brightnessValue) {
+ public void setBrightness(float brightnessValue, int userSerial) {
// Update the setting, which will eventually call back into DPC to have us actually update
// the display with the new value.
+ mBrightnessSetting.setUserSerial(userSerial);
mBrightnessSetting.setBrightness(brightnessValue);
if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
float nits = convertToNits(brightnessValue);
@@ -3003,8 +3001,8 @@
mScreenOffBrightnessSensorController.dump(pw);
}
- if (mHbmController != null) {
- mHbmController.dump(pw);
+ if (mBrightnessRangeController != null) {
+ mBrightnessRangeController.dump(pw);
}
if (mBrightnessThrottler != null) {
@@ -3471,7 +3469,8 @@
HysteresisLevels screenBrightnessThresholds,
HysteresisLevels ambientBrightnessThresholdsIdle,
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
- HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
+ BrightnessRangeController brightnessRangeController,
+ BrightnessThrottler brightnessThrottler,
BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
int ambientLightHorizonLong, float userLux, float userBrightness) {
return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
@@ -3480,9 +3479,9 @@
brighteningLightDebounceConfig, darkeningLightDebounceConfig,
resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
- screenBrightnessThresholdsIdle, context, hbmController, brightnessThrottler,
- idleModeBrightnessMapper, ambientLightHorizonShort, ambientLightHorizonLong,
- userLux, userBrightness);
+ screenBrightnessThresholdsIdle, context, brightnessRangeController,
+ brightnessThrottler, idleModeBrightnessMapper, ambientLightHorizonShort,
+ ambientLightHorizonLong, userLux, userBrightness);
}
BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 41e4671d..040cecc 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -376,8 +376,7 @@
private final ColorDisplayServiceInternal mCdsi;
private float[] mNitsRange;
- private final HighBrightnessModeController mHbmController;
- private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
+ private final BrightnessRangeController mBrightnessRangeController;
private final BrightnessThrottler mBrightnessThrottler;
@@ -489,7 +488,6 @@
mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
() -> updatePowerState(), mDisplayId, mSensorManager);
- mHighBrightnessModeMetadata = hbmMetadata;
mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(context, mDisplayId);
mTag = "DisplayPowerController2[" + mDisplayId + "]";
@@ -532,9 +530,22 @@
mSkipScreenOnBrightnessRamp = resources.getBoolean(
R.bool.config_skipScreenOnBrightnessRamp);
- mHbmController = createHbmControllerLocked();
+ Runnable modeChangeCallback = () -> {
+ sendUpdatePowerState();
+ postBrightnessChangeRunnable();
+ // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern.
+ if (mAutomaticBrightnessController != null) {
+ mAutomaticBrightnessController.update();
+ }
+ };
+ HighBrightnessModeController hbmController = createHbmControllerLocked(hbmMetadata,
+ modeChangeCallback);
mBrightnessThrottler = createBrightnessThrottlerLocked();
+
+ mBrightnessRangeController = new BrightnessRangeController(hbmController,
+ modeChangeCallback);
+
mDisplayBrightnessController =
new DisplayBrightnessController(context, null,
mDisplayId, mLogicalDisplay.getDisplayInfoLocked().brightnessDefault,
@@ -848,17 +859,8 @@
mBrightnessRampIncreaseMaxTimeMillis,
mBrightnessRampDecreaseMaxTimeMillis);
}
- mHbmController.setHighBrightnessModeMetadata(hbmMetadata);
- mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId,
- mDisplayDeviceConfig.getHighBrightnessModeData(),
- new HighBrightnessModeController.HdrBrightnessDeviceConfig() {
- @Override
- public float getHdrBrightnessFromSdr(
- float sdrBrightness, float maxDesiredHdrSdrRatio) {
- return mDisplayDeviceConfig.getHdrBrightnessFromSdr(
- sdrBrightness, maxDesiredHdrSdrRatio);
- }
- });
+
+ mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);
mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(
mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(),
mThermalBrightnessThrottlingDataId, mUniqueDisplayId);
@@ -1076,7 +1078,7 @@
darkeningLightDebounce, autoBrightnessResetAmbientLuxAfterWarmUp,
ambientBrightnessThresholds, screenBrightnessThresholds,
ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext,
- mHbmController, mBrightnessThrottler, mIdleModeBrightnessMapper,
+ mBrightnessRangeController, mBrightnessThrottler, mIdleModeBrightnessMapper,
mDisplayDeviceConfig.getAmbientHorizonShort(),
mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userBrightness);
mDisplayBrightnessController.setAutomaticBrightnessController(
@@ -1180,7 +1182,7 @@
/** Clean up all resources that are accessed via the {@link #mHandler} thread. */
private void cleanupHandlerThreadAfterStop() {
mDisplayPowerProximityStateController.cleanup();
- mHbmController.stop();
+ mBrightnessRangeController.stop();
mBrightnessThrottler.stop();
mHandler.removeCallbacksAndMessages(null);
@@ -1295,7 +1297,7 @@
&& (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()
|| userSetBrightnessChanged);
- mHbmController.setAutoBrightnessEnabled(mAutomaticBrightnessStrategy
+ mBrightnessRangeController.setAutoBrightnessEnabled(mAutomaticBrightnessStrategy
.shouldUseAutoBrightness()
? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
: AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED);
@@ -1452,7 +1454,7 @@
// here instead of having HbmController listen to the brightness setting because certain
// brightness sources (such as an app override) are not saved to the setting, but should be
// reflected in HBM calculations.
- mHbmController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
+ mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,
mBrightnessThrottler.getBrightnessMaxReason());
// Animate the screen brightness when the screen is on or dozing.
@@ -1509,13 +1511,14 @@
float sdrAnimateValue = animateValue;
// TODO(b/216365040): The decision to prevent HBM for HDR in low power mode should be
// done in HighBrightnessModeController.
- if (mHbmController.getHighBrightnessMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR
+ if (mBrightnessRangeController.getHighBrightnessMode()
+ == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR
&& (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_DIMMED) == 0
&& (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_LOW_POWER)
== 0) {
// We want to scale HDR brightness level with the SDR level, we also need to restore
// SDR brightness immediately when entering dim or low power mode.
- animateValue = mHbmController.getHdrBrightnessValue();
+ animateValue = mBrightnessRangeController.getHdrBrightnessValue();
}
final float currentBrightness = mPowerState.getScreenBrightness();
@@ -1579,8 +1582,8 @@
mTempBrightnessEvent.setBrightness(brightnessState);
mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId);
mTempBrightnessEvent.setReason(mBrightnessReason);
- mTempBrightnessEvent.setHbmMax(mHbmController.getCurrentBrightnessMax());
- mTempBrightnessEvent.setHbmMode(mHbmController.getHighBrightnessMode());
+ mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax());
+ mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());
mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
| (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
| (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0));
@@ -1750,9 +1753,11 @@
private boolean saveBrightnessInfo(float brightness, float adjustedBrightness) {
synchronized (mCachedBrightnessInfo) {
- final float minBrightness = Math.min(mHbmController.getCurrentBrightnessMin(),
+ final float minBrightness = Math.min(
+ mBrightnessRangeController.getCurrentBrightnessMin(),
mBrightnessThrottler.getBrightnessCap());
- final float maxBrightness = Math.min(mHbmController.getCurrentBrightnessMax(),
+ final float maxBrightness = Math.min(
+ mBrightnessRangeController.getCurrentBrightnessMax(),
mBrightnessThrottler.getBrightnessCap());
boolean changed = false;
@@ -1770,10 +1775,10 @@
maxBrightness);
changed |=
mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.hbmMode,
- mHbmController.getHighBrightnessMode());
+ mBrightnessRangeController.getHighBrightnessMode());
changed |=
mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.hbmTransitionPoint,
- mHbmController.getTransitionPoint());
+ mBrightnessRangeController.getTransitionPoint());
changed |=
mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,
mBrightnessThrottler.getBrightnessMaxReason());
@@ -1786,7 +1791,8 @@
mHandler.post(mOnBrightnessChangeRunnable);
}
- private HighBrightnessModeController createHbmControllerLocked() {
+ private HighBrightnessModeController createHbmControllerLocked(
+ HighBrightnessModeMetadata hbmMetadata, Runnable modeChangeCallback) {
final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();
final IBinder displayToken =
@@ -1798,22 +1804,9 @@
final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
return new HighBrightnessModeController(mHandler, info.width, info.height, displayToken,
displayUniqueId, PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, hbmData,
- new HighBrightnessModeController.HdrBrightnessDeviceConfig() {
- @Override
- public float getHdrBrightnessFromSdr(
- float sdrBrightness, float maxDesiredHdrSdrRatio) {
- return mDisplayDeviceConfig.getHdrBrightnessFromSdr(
- sdrBrightness, maxDesiredHdrSdrRatio);
- }
- },
- () -> {
- sendUpdatePowerState();
- postBrightnessChangeRunnable();
- // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern.
- if (mAutomaticBrightnessController != null) {
- mAutomaticBrightnessController.update();
- }
- }, mHighBrightnessModeMetadata, mContext);
+ (sdrBrightness, maxDesiredHdrSdrRatio) ->
+ mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness,
+ maxDesiredHdrSdrRatio), modeChangeCallback, hbmMetadata, mContext);
}
private BrightnessThrottler createBrightnessThrottlerLocked() {
@@ -1960,8 +1953,8 @@
if (Float.isNaN(value)) {
value = PowerManager.BRIGHTNESS_MIN;
}
- return MathUtils.constrain(value,
- mHbmController.getCurrentBrightnessMin(), mHbmController.getCurrentBrightnessMax());
+ return MathUtils.constrain(value, mBrightnessRangeController.getCurrentBrightnessMin(),
+ mBrightnessRangeController.getCurrentBrightnessMax());
}
private void animateScreenBrightness(float target, float sdrTarget, float rate) {
@@ -2179,8 +2172,8 @@
}
@Override
- public void setBrightness(float brightnessValue) {
- mDisplayBrightnessController.setBrightness(brightnessValue);
+ public void setBrightness(float brightnessValue, int userSerial) {
+ mDisplayBrightnessController.setBrightness(brightnessValue, userSerial);
}
@Override
@@ -2195,7 +2188,7 @@
@Override
public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux) {
- mHbmController.onAmbientLuxChange(ambientLux);
+ mBrightnessRangeController.onAmbientLuxChange(ambientLux);
if (nits < 0) {
mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness);
} else {
@@ -2374,8 +2367,8 @@
dumpRbcEvents(pw);
- if (mHbmController != null) {
- mHbmController.dump(pw);
+ if (mBrightnessRangeController != null) {
+ mBrightnessRangeController.dump(pw);
}
if (mBrightnessThrottler != null) {
@@ -2840,7 +2833,8 @@
HysteresisLevels screenBrightnessThresholds,
HysteresisLevels ambientBrightnessThresholdsIdle,
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
- HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
+ BrightnessRangeController brightnessModeController,
+ BrightnessThrottler brightnessThrottler,
BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
int ambientLightHorizonLong, float userLux, float userBrightness) {
return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
@@ -2849,9 +2843,9 @@
brighteningLightDebounceConfig, darkeningLightDebounceConfig,
resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
- screenBrightnessThresholdsIdle, context, hbmController, brightnessThrottler,
- idleModeBrightnessMapper, ambientLightHorizonShort, ambientLightHorizonLong,
- userLux, userBrightness);
+ screenBrightnessThresholdsIdle, context, brightnessModeController,
+ brightnessThrottler, idleModeBrightnessMapper, ambientLightHorizonShort,
+ ambientLightHorizonLong, userLux, userBrightness);
}
BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index 73edb97..5fbbcbd 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -29,7 +29,7 @@
* An interface to manage the display's power state and brightness
*/
public interface DisplayPowerControllerInterface {
-
+ int DEFAULT_USER_SERIAL = -1;
/**
* Notified when the display is changed.
*
@@ -98,7 +98,17 @@
* Set the screen brightness of the associated display
* @param brightness The value to which the brightness is to be set
*/
- void setBrightness(float brightness);
+ default void setBrightness(float brightness) {
+ setBrightness(brightness, DEFAULT_USER_SERIAL);
+ }
+
+ /**
+ * Set the screen brightness of the associated display
+ * @param brightness The value to which the brightness is to be set
+ * @param userSerial The user for which the brightness value is to be set. Use userSerial = -1,
+ * if brightness needs to be updated for the current user.
+ */
+ void setBrightness(float brightness, int userSerial);
/**
* Checks if the proximity sensor is available
diff --git a/services/core/java/com/android/server/display/NormalBrightnessModeController.java b/services/core/java/com/android/server/display/NormalBrightnessModeController.java
new file mode 100644
index 0000000..91e4a9e
--- /dev/null
+++ b/services/core/java/com/android/server/display/NormalBrightnessModeController.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import android.os.PowerManager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class NormalBrightnessModeController {
+ private Map<Float, Float> mTransitionPoints = new HashMap<>();
+
+ // brightness limit in normal brightness mode, based on ambient lux.
+ private float mVirtualTransitionPoint = PowerManager.BRIGHTNESS_MAX;
+
+ boolean onAmbientLuxChange(float ambientLux) {
+ float currentAmbientBoundary = Float.MAX_VALUE;
+ float currentTransitionPoint = PowerManager.BRIGHTNESS_MAX;
+ for (Map.Entry<Float, Float> transitionPoint: mTransitionPoints.entrySet()) {
+ float ambientBoundary = transitionPoint.getKey();
+ // find ambient lux upper boundary closest to current ambient lux
+ if (ambientBoundary > ambientLux && ambientBoundary < currentAmbientBoundary) {
+ currentTransitionPoint = transitionPoint.getValue();
+ currentAmbientBoundary = ambientBoundary;
+ }
+ }
+ if (mVirtualTransitionPoint != currentTransitionPoint) {
+ mVirtualTransitionPoint = currentTransitionPoint;
+ return true;
+ }
+ return false;
+ }
+
+ float getCurrentBrightnessMax() {
+ return mVirtualTransitionPoint;
+ }
+}
diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index 6d6ed72..2d7792d 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -133,6 +133,7 @@
private static final String TAG_BRIGHTNESS_NITS_FOR_DEFAULT_DISPLAY =
"brightness-nits-for-default-display";
+ public static final int DEFAULT_USER_ID = -1;
// Remembered Wifi display devices.
private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>();
@@ -294,7 +295,7 @@
return false;
}
- public float getBrightness(DisplayDevice device) {
+ public float getBrightness(DisplayDevice device, int userSerial) {
if (device == null || !device.hasStableUniqueId()) {
return Float.NaN;
}
@@ -302,10 +303,10 @@
if (state == null) {
return Float.NaN;
}
- return state.getBrightness();
+ return state.getBrightness(userSerial);
}
- public boolean setBrightness(DisplayDevice displayDevice, float brightness) {
+ public boolean setBrightness(DisplayDevice displayDevice, float brightness, int userSerial) {
if (displayDevice == null || !displayDevice.hasStableUniqueId()) {
return false;
}
@@ -314,7 +315,7 @@
return false;
}
final DisplayState state = getDisplayState(displayDeviceUniqueId, true);
- if (state.setBrightness(brightness)) {
+ if (state.setBrightness(brightness, userSerial)) {
setDirty();
return true;
}
@@ -611,6 +612,7 @@
state.saveToXml(serializer);
serializer.endTag(null, TAG_DISPLAY);
}
+
serializer.endTag(null, TAG_DISPLAY_STATES);
serializer.startTag(null, TAG_STABLE_DEVICE_VALUES);
mStableDeviceValues.saveToXml(serializer);
@@ -649,7 +651,8 @@
private static final class DisplayState {
private int mColorMode;
- private float mBrightness = Float.NaN;
+
+ private SparseArray<Float> mPerUserBrightness = new SparseArray<>();
private int mWidth;
private int mHeight;
private float mRefreshRate;
@@ -670,16 +673,25 @@
return mColorMode;
}
- public boolean setBrightness(float brightness) {
- if (brightness == mBrightness) {
+ public boolean setBrightness(float brightness, int userSerial) {
+ // Remove the default user brightness, before setting a new user-specific value.
+ // This is a one-time operation, required to restructure the config after user-specific
+ // brightness was introduced.
+ mPerUserBrightness.remove(DEFAULT_USER_ID);
+
+ if (getBrightness(userSerial) == brightness) {
return false;
}
- mBrightness = brightness;
+ mPerUserBrightness.set(userSerial, brightness);
return true;
}
- public float getBrightness() {
- return mBrightness;
+ public float getBrightness(int userSerial) {
+ float brightness = mPerUserBrightness.get(userSerial, Float.NaN);
+ if (Float.isNaN(brightness)) {
+ brightness = mPerUserBrightness.get(DEFAULT_USER_ID, Float.NaN);
+ }
+ return brightness;
}
public boolean setBrightnessConfiguration(BrightnessConfiguration configuration,
@@ -729,12 +741,7 @@
mColorMode = Integer.parseInt(value);
break;
case TAG_BRIGHTNESS_VALUE:
- String brightness = parser.nextText();
- try {
- mBrightness = Float.parseFloat(brightness);
- } catch (NumberFormatException e) {
- mBrightness = Float.NaN;
- }
+ loadBrightnessFromXml(parser);
break;
case TAG_BRIGHTNESS_CONFIGURATIONS:
mDisplayBrightnessConfigurations.loadFromXml(parser);
@@ -760,11 +767,12 @@
serializer.text(Integer.toString(mColorMode));
serializer.endTag(null, TAG_COLOR_MODE);
- serializer.startTag(null, TAG_BRIGHTNESS_VALUE);
- if (!Float.isNaN(mBrightness)) {
- serializer.text(Float.toString(mBrightness));
+ for (int i = 0; i < mPerUserBrightness.size(); i++) {
+ serializer.startTag(null, TAG_BRIGHTNESS_VALUE);
+ serializer.attributeInt(null, ATTR_USER_SERIAL, mPerUserBrightness.keyAt(i));
+ serializer.text(Float.toString(mPerUserBrightness.valueAt(i)));
+ serializer.endTag(null, TAG_BRIGHTNESS_VALUE);
}
- serializer.endTag(null, TAG_BRIGHTNESS_VALUE);
serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS);
mDisplayBrightnessConfigurations.saveToXml(serializer);
@@ -785,12 +793,33 @@
public void dump(final PrintWriter pw, final String prefix) {
pw.println(prefix + "ColorMode=" + mColorMode);
- pw.println(prefix + "BrightnessValue=" + mBrightness);
+ pw.println(prefix + "BrightnessValues: ");
+ for (int i = 0; i < mPerUserBrightness.size(); i++) {
+ pw.println("User: " + mPerUserBrightness.keyAt(i)
+ + " Value: " + mPerUserBrightness.valueAt(i));
+ }
pw.println(prefix + "DisplayBrightnessConfigurations: ");
mDisplayBrightnessConfigurations.dump(pw, prefix);
pw.println(prefix + "Resolution=" + mWidth + " " + mHeight);
pw.println(prefix + "RefreshRate=" + mRefreshRate);
}
+
+ private void loadBrightnessFromXml(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ int userSerial;
+ try {
+ userSerial = parser.getAttributeInt(null, ATTR_USER_SERIAL);
+ } catch (NumberFormatException | XmlPullParserException e) {
+ userSerial = DEFAULT_USER_ID;
+ Slog.e(TAG, "Failed to read user serial", e);
+ }
+ String brightness = parser.nextText();
+ try {
+ mPerUserBrightness.set(userSerial, Float.parseFloat(brightness));
+ } catch (NumberFormatException nfe) {
+ Slog.e(TAG, "Failed to read brightness", nfe);
+ }
+ }
}
private static final class StableDeviceValues {
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index 7574de8..2f52b70 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -38,6 +38,8 @@
* display. Applies the chosen brightness.
*/
public final class DisplayBrightnessController {
+ private static final int DEFAULT_USER_SERIAL = -1;
+
// The ID of the display tied to this DisplayBrightnessController
private final int mDisplayId;
@@ -274,8 +276,16 @@
* Notifies the brightnessSetting to persist the supplied brightness value.
*/
public void setBrightness(float brightnessValue) {
+ setBrightness(brightnessValue, DEFAULT_USER_SERIAL);
+ }
+
+ /**
+ * Notifies the brightnessSetting to persist the supplied brightness value for a user.
+ */
+ public void setBrightness(float brightnessValue, int userSerial) {
// Update the setting, which will eventually call back into DPC to have us actually
// update the display with the new value.
+ mBrightnessSetting.setUserSerial(userSerial);
mBrightnessSetting.setBrightness(brightnessValue);
if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) {
float nits = convertToNits(brightnessValue);
diff --git a/services/core/java/com/android/server/input/InputFeatureFlagProvider.java b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java
new file mode 100644
index 0000000..7c7f151
--- /dev/null
+++ b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input;
+
+import android.sysprop.InputProperties;
+
+import java.util.Optional;
+
+/**
+ * A component of {@link InputManagerService} responsible for managing the input sysprop flags
+ *
+ * @hide
+ */
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public final class InputFeatureFlagProvider {
+
+ // To disable Keyboard backlight control via Framework, run:
+ // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart)
+ private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED =
+ InputProperties.enable_keyboard_backlight_control().orElse(true);
+
+ // To disable Framework controlled keyboard backlight animation run:
+ // adb shell setprop persist.input.keyboard.backlight_animation.enabled false (requires restart)
+ private static final boolean KEYBOARD_BACKLIGHT_ANIMATION_ENABLED =
+ InputProperties.enable_keyboard_backlight_animation().orElse(false);
+
+ // To disable Custom keyboard backlight levels support via IDC files run:
+ // adb shell setprop persist.input.keyboard_backlight_custom_levels.enabled false (requires
+ // restart)
+ private static final boolean KEYBOARD_BACKLIGHT_CUSTOM_LEVELS_ENABLED =
+ InputProperties.enable_keyboard_backlight_custom_levels().orElse(true);
+
+ private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty();
+ private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty();
+ private static Optional<Boolean> sKeyboardBacklightCustomLevelsOverride = Optional.empty();
+
+ public static boolean isKeyboardBacklightControlEnabled() {
+ return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED);
+ }
+
+ public static boolean isKeyboardBacklightAnimationEnabled() {
+ return sKeyboardBacklightAnimationOverride.orElse(KEYBOARD_BACKLIGHT_ANIMATION_ENABLED);
+ }
+
+ public static boolean isKeyboardBacklightCustomLevelsEnabled() {
+ return sKeyboardBacklightCustomLevelsOverride.orElse(
+ KEYBOARD_BACKLIGHT_CUSTOM_LEVELS_ENABLED);
+ }
+
+ public static void setKeyboardBacklightControlEnabled(boolean enabled) {
+ sKeyboardBacklightControlOverride = Optional.of(enabled);
+ }
+
+ public static void setKeyboardBacklightAnimationEnabled(boolean enabled) {
+ sKeyboardBacklightAnimationOverride = Optional.of(enabled);
+ }
+
+ public static void setKeyboardBacklightCustomLevelsEnabled(boolean enabled) {
+ sKeyboardBacklightCustomLevelsOverride = Optional.of(enabled);
+ }
+
+ /**
+ * Clears all input feature flag overrides.
+ */
+ public static void clearOverrides() {
+ sKeyboardBacklightControlOverride = Optional.empty();
+ sKeyboardBacklightAnimationOverride = Optional.empty();
+ sKeyboardBacklightCustomLevelsOverride = Optional.empty();
+ }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 4cb22db..c7b80af 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -74,7 +74,6 @@
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
-import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.VibrationEffect;
import android.os.vibrator.StepSegment;
@@ -160,11 +159,6 @@
private static final AdditionalDisplayInputProperties
DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties();
- // To disable Keyboard backlight control via Framework, run:
- // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart)
- private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean(
- "persist.input.keyboard.backlight_control.enabled", true);
-
private final NativeInputManagerService mNative;
private final Context mContext;
@@ -439,10 +433,9 @@
mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore,
injector.getLooper());
mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
- mKeyboardBacklightController =
- KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext,
- mNative, mDataStore, injector.getLooper())
- : new KeyboardBacklightControllerInterface() {};
+ mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled()
+ ? new KeyboardBacklightController(mContext, mNative, mDataStore,
+ injector.getLooper()) : new KeyboardBacklightControllerInterface() {};
mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
mUseDevInputEventForAudioJack =
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 651063e..42591f4 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -115,35 +115,34 @@
return setting != 0;
}
- private int getPointerSpeedValue(String settingName) {
- int speed = Settings.System.getIntForUser(mContext.getContentResolver(),
- settingName, InputSettings.DEFAULT_POINTER_SPEED, UserHandle.USER_CURRENT);
+ private int constrainPointerSpeedValue(int speed) {
return Math.min(Math.max(speed, InputSettings.MIN_POINTER_SPEED),
InputSettings.MAX_POINTER_SPEED);
}
private void updateMousePointerSpeed() {
- mNative.setPointerSpeed(getPointerSpeedValue(Settings.System.POINTER_SPEED));
+ int speed = Settings.System.getIntForUser(mContext.getContentResolver(),
+ Settings.System.POINTER_SPEED, InputSettings.DEFAULT_POINTER_SPEED,
+ UserHandle.USER_CURRENT);
+ mNative.setPointerSpeed(constrainPointerSpeedValue(speed));
}
private void updateTouchpadPointerSpeed() {
mNative.setTouchpadPointerSpeed(
- getPointerSpeedValue(Settings.System.TOUCHPAD_POINTER_SPEED));
+ constrainPointerSpeedValue(InputSettings.getTouchpadPointerSpeed(mContext)));
}
private void updateTouchpadNaturalScrollingEnabled() {
mNative.setTouchpadNaturalScrollingEnabled(
- getBoolean(Settings.System.TOUCHPAD_NATURAL_SCROLLING, true));
+ InputSettings.useTouchpadNaturalScrolling(mContext));
}
private void updateTouchpadTapToClickEnabled() {
- mNative.setTouchpadTapToClickEnabled(
- getBoolean(Settings.System.TOUCHPAD_TAP_TO_CLICK, true));
+ mNative.setTouchpadTapToClickEnabled(InputSettings.useTouchpadTapToClick(mContext));
}
private void updateTouchpadRightClickZoneEnabled() {
- mNative.setTouchpadRightClickZoneEnabled(
- getBoolean(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, false));
+ mNative.setTouchpadRightClickZoneEnabled(InputSettings.useTouchpadRightClickZone(mContext));
}
private void updateShowTouches() {
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index 48c346a..36238a8 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -16,6 +16,7 @@
package com.android.server.input;
+import android.animation.ValueAnimator;
import android.annotation.BinderThread;
import android.content.Context;
import android.graphics.Color;
@@ -45,6 +46,7 @@
import java.util.Arrays;
import java.util.Objects;
import java.util.OptionalInt;
+import java.util.TreeSet;
/**
* A thread-safe component of {@link InputManagerService} responsible for managing the keyboard
@@ -69,7 +71,11 @@
private static final int MSG_NOTIFY_USER_INACTIVITY = 5;
private static final int MSG_INTERACTIVE_STATE_CHANGED = 6;
private static final int MAX_BRIGHTNESS = 255;
- private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10;
+ private static final int DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS = 10;
+ @VisibleForTesting
+ static final int MAX_BRIGHTNESS_CHANGE_STEPS = 10;
+ private static final long TRANSITION_ANIMATION_DURATION_MILLIS =
+ Duration.ofSeconds(1).toMillis();
private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight";
@@ -77,7 +83,8 @@
static final long USER_INACTIVITY_THRESHOLD_MILLIS = Duration.ofSeconds(30).toMillis();
@VisibleForTesting
- static final int[] BRIGHTNESS_VALUE_FOR_LEVEL = new int[NUM_BRIGHTNESS_CHANGE_STEPS + 1];
+ static final int[] DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL =
+ new int[DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS + 1];
private final Context mContext;
private final NativeInputManagerService mNative;
@@ -85,6 +92,7 @@
@GuardedBy("mDataStore")
private final PersistentDataStore mDataStore;
private final Handler mHandler;
+ private final AnimatorFactory mAnimatorFactory;
// Always access on handler thread or need to lock this for synchronization.
private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1);
// Maintains state if all backlights should be on or turned off
@@ -100,19 +108,26 @@
static {
// Fixed brightness levels to avoid issues when converting back and forth from the
// device brightness range to [0-255]
- // Levels are: 0, 25, 51, ..., 255
- for (int i = 0; i <= NUM_BRIGHTNESS_CHANGE_STEPS; i++) {
- BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor(
- ((float) i * MAX_BRIGHTNESS) / NUM_BRIGHTNESS_CHANGE_STEPS);
+ // Levels are: 0, 51, ..., 255
+ for (int i = 0; i <= DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS; i++) {
+ DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor(
+ ((float) i * MAX_BRIGHTNESS) / DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS);
}
}
KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
PersistentDataStore dataStore, Looper looper) {
+ this(context, nativeService, dataStore, looper, ValueAnimator::ofInt);
+ }
+
+ @VisibleForTesting
+ KeyboardBacklightController(Context context, NativeInputManagerService nativeService,
+ PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory) {
mContext = context;
mNative = nativeService;
mDataStore = dataStore;
mHandler = new Handler(looper, this::handleMessage);
+ mAnimatorFactory = animatorFactory;
}
@Override
@@ -173,18 +188,18 @@
final int currBrightnessLevel = state.mBrightnessLevel;
final int newBrightnessLevel;
if (direction == Direction.DIRECTION_UP) {
- newBrightnessLevel = Math.min(currBrightnessLevel + 1, NUM_BRIGHTNESS_CHANGE_STEPS);
+ newBrightnessLevel = Math.min(currBrightnessLevel + 1,
+ state.getNumBrightnessChangeSteps());
} else {
newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0);
}
- updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel,
- true /* isTriggeredByKeyPress */);
+ updateBacklightState(deviceId, newBrightnessLevel, true /* isTriggeredByKeyPress */);
synchronized (mDataStore) {
try {
mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(),
keyboardBacklight.getId(),
- BRIGHTNESS_VALUE_FOR_LEVEL[newBrightnessLevel]);
+ state.mBrightnessValueForLevel[newBrightnessLevel]);
} finally {
mDataStore.saveIfNeeded();
}
@@ -192,6 +207,7 @@
}
private void restoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) {
+ KeyboardBacklightState state = mKeyboardBacklights.get(inputDevice.getId());
OptionalInt brightness;
synchronized (mDataStore) {
brightness = mDataStore.getKeyboardBacklightBrightness(
@@ -199,12 +215,11 @@
}
if (brightness.isPresent()) {
int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt()));
- int index = Arrays.binarySearch(BRIGHTNESS_VALUE_FOR_LEVEL, brightnessValue);
+ int index = Arrays.binarySearch(state.mBrightnessValueForLevel, brightnessValue);
if (index < 0) {
- index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1));
+ index = Math.min(state.getNumBrightnessChangeSteps(), -(index + 1));
}
- updateBacklightState(inputDevice.getId(), keyboardBacklight, index,
- false /* isTriggeredByKeyPress */);
+ updateBacklightState(inputDevice.getId(), index, false /* isTriggeredByKeyPress */);
if (DEBUG) {
Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt());
}
@@ -217,14 +232,10 @@
if (!mIsInteractive) {
return;
}
- if (!mIsBacklightOn) {
- mIsBacklightOn = true;
- for (int i = 0; i < mKeyboardBacklights.size(); i++) {
- int deviceId = mKeyboardBacklights.keyAt(i);
- KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
- updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel,
- false /* isTriggeredByKeyPress */);
- }
+ mIsBacklightOn = true;
+ for (int i = 0; i < mKeyboardBacklights.size(); i++) {
+ KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+ state.onBacklightStateChanged();
}
mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY);
mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY,
@@ -232,14 +243,10 @@
}
private void handleUserInactivity() {
- if (mIsBacklightOn) {
- mIsBacklightOn = false;
- for (int i = 0; i < mKeyboardBacklights.size(); i++) {
- int deviceId = mKeyboardBacklights.keyAt(i);
- KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
- updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel,
- false /* isTriggeredByKeyPress */);
- }
+ mIsBacklightOn = false;
+ for (int i = 0; i < mKeyboardBacklights.size(); i++) {
+ KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
+ state.onBacklightStateChanged();
}
}
@@ -310,7 +317,7 @@
return;
}
// The keyboard backlight was added or changed.
- mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight));
+ mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight));
restoreBacklightBrightness(inputDevice, keyboardBacklight);
}
@@ -372,31 +379,28 @@
}
}
- private void updateBacklightState(int deviceId, Light light, int brightnessLevel,
+ private void updateBacklightState(int deviceId, int brightnessLevel,
boolean isTriggeredByKeyPress) {
KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
if (state == null) {
return;
}
- mNative.setLightColor(deviceId, light.getId(),
- mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0)
- : 0);
- if (DEBUG) {
- Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel
- + "(isBacklightOn = " + mIsBacklightOn + ")");
- }
- state.mBrightnessLevel = brightnessLevel;
+ state.setBrightnessLevel(brightnessLevel);
synchronized (mKeyboardBacklightListenerRecords) {
for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
IKeyboardBacklightState callbackState = new IKeyboardBacklightState();
callbackState.brightnessLevel = brightnessLevel;
- callbackState.maxBrightnessLevel = NUM_BRIGHTNESS_CHANGE_STEPS;
+ callbackState.maxBrightnessLevel = state.getNumBrightnessChangeSteps();
mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged(
deviceId, callbackState, isTriggeredByKeyPress);
}
}
+
+ if (DEBUG) {
+ Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel);
+ }
}
private void onKeyboardBacklightListenerDied(int pid) {
@@ -436,10 +440,7 @@
@Override
public void dump(PrintWriter pw) {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
- ipw.println(
- TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = "
- + mIsBacklightOn);
-
+ ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights");
ipw.increaseIndent();
for (int i = 0; i < mKeyboardBacklights.size(); i++) {
KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
@@ -478,12 +479,87 @@
}
}
- private static class KeyboardBacklightState {
+ private class KeyboardBacklightState {
+ private final int mDeviceId;
private final Light mLight;
private int mBrightnessLevel;
+ private ValueAnimator mAnimator;
+ private final int[] mBrightnessValueForLevel;
- KeyboardBacklightState(Light light) {
+ KeyboardBacklightState(int deviceId, Light light) {
+ mDeviceId = deviceId;
mLight = light;
+ mBrightnessValueForLevel = setupBrightnessLevels();
+ }
+
+ private int[] setupBrightnessLevels() {
+ if (!InputFeatureFlagProvider.isKeyboardBacklightCustomLevelsEnabled()) {
+ return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
+ }
+ int[] customLevels = mLight.getPreferredBrightnessLevels();
+ if (customLevels == null || customLevels.length == 0) {
+ return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
+ }
+ TreeSet<Integer> brightnessLevels = new TreeSet<>();
+ brightnessLevels.add(0);
+ for (int level : customLevels) {
+ if (level > 0 && level < MAX_BRIGHTNESS) {
+ brightnessLevels.add(level);
+ }
+ }
+ brightnessLevels.add(MAX_BRIGHTNESS);
+ int brightnessChangeSteps = brightnessLevels.size() - 1;
+ if (brightnessChangeSteps > MAX_BRIGHTNESS_CHANGE_STEPS) {
+ return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL;
+ }
+ int[] result = new int[brightnessLevels.size()];
+ int index = 0;
+ for (int val : brightnessLevels) {
+ result[index++] = val;
+ }
+ return result;
+ }
+
+ private int getNumBrightnessChangeSteps() {
+ return mBrightnessValueForLevel.length - 1;
+ }
+
+ private void onBacklightStateChanged() {
+ setBacklightValue(mIsBacklightOn ? mBrightnessValueForLevel[mBrightnessLevel] : 0);
+ }
+ private void setBrightnessLevel(int brightnessLevel) {
+ if (mIsBacklightOn) {
+ setBacklightValue(mBrightnessValueForLevel[brightnessLevel]);
+ }
+ mBrightnessLevel = brightnessLevel;
+ }
+
+ private void cancelAnimation() {
+ if (mAnimator != null && mAnimator.isRunning()) {
+ mAnimator.cancel();
+ }
+ }
+
+ private void setBacklightValue(int toValue) {
+ int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId()));
+ if (fromValue == toValue) {
+ return;
+ }
+ if (InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled()) {
+ startAnimation(fromValue, toValue);
+ } else {
+ mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0));
+ }
+ }
+
+ private void startAnimation(int fromValue, int toValue) {
+ // Cancel any ongoing animation before starting a new one
+ cancelAnimation();
+ mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue);
+ mAnimator.addUpdateListener(
+ (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(),
+ Color.argb((int) animation.getAnimatedValue(), 0, 0, 0)));
+ mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start();
}
@Override
@@ -493,4 +569,9 @@
+ "}";
}
}
+
+ @VisibleForTesting
+ interface AnimatorFactory {
+ ValueAnimator makeIntAnimator(int from, int to);
+ }
}
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 6ec4022..611a61a2 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -252,6 +252,8 @@
if (needToShowNotification) {
maybeUpdateNotification();
}
+ // TODO (b/280421650): Implement logging statements using KeyboardMetricsCollector
+ // for KeyboardConfigured atom
}
private String getDefaultKeyboardLayout(final InputDevice inputDevice) {
diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
new file mode 100644
index 0000000..b8f57f5
--- /dev/null
+++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.hardware.input.KeyboardLayout;
+import android.util.proto.ProtoOutputStream;
+import android.view.InputDevice;
+
+import com.android.internal.os.KeyboardConfiguredProto.KeyboardLayoutConfig;
+import com.android.internal.os.KeyboardConfiguredProto.RepeatedKeyboardLayoutConfig;
+import com.android.internal.util.FrameworkStatsLog;
+
+import java.lang.annotation.Retention;
+import java.util.List;
+
+/**
+ * Collect Keyboard metrics
+ */
+public final class KeyboardMetricsCollector {
+ private static final String TAG = "KeyboardMetricCollector";
+
+ /**
+ * Log keyboard system shortcuts for the proto
+ * {@link com.android.os.input.KeyboardSystemsEventReported}
+ * defined in "stats/atoms/input/input_extension_atoms.proto"
+ */
+ public static void logKeyboardSystemsEventReportedAtom(InputDevice inputDevice,
+ int keyboardSystemEvent, int[] keyCode, int modifierState) {
+ int vendor_id = inputDevice.getVendorId();
+ int product_id = inputDevice.getProductId();
+ FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED,
+ vendor_id, product_id, keyboardSystemEvent, keyCode, modifierState);
+ }
+
+ /**
+ * Function to log the KeyboardConfigured
+ * {@link com.android.os.input.KeyboardConfigured} atom
+ *
+ * @param inputDevice Input device
+ * @param keyboardLayoutConfigurations List of keyboard configurations
+ * @param isFirstTimeConfiguration Whether keyboard is configured for the first time
+ */
+ public static void logKeyboardConfiguredAtom(InputDevice inputDevice,
+ List<KeyboardLayoutConfiguration> keyboardLayoutConfigurations,
+ boolean isFirstTimeConfiguration) {
+ int vendor_id = inputDevice.getVendorId();
+ int product_id = inputDevice.getProductId();
+
+ // Creating proto to log nested field KeyboardLayoutConfig in atom
+ ProtoOutputStream proto = new ProtoOutputStream();
+
+ for (KeyboardLayoutConfiguration keyboardLayoutConfiguration :
+ keyboardLayoutConfigurations) {
+ addKeyboardLayoutConfigurationToProto(proto, keyboardLayoutConfiguration);
+ }
+ // Push the atom to Statsd
+ FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_CONFIGURED,
+ isFirstTimeConfiguration, vendor_id, product_id, proto.getBytes());
+ }
+
+ /**
+ * Populate the KeyboardLayoutConfig proto which is a repeated proto
+ * in the RepeatedKeyboardLayoutConfig proto with values from the
+ * {@link KeyboardLayoutConfiguration} class
+ * The proto definitions can be found at:
+ * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto"
+ *
+ * @param proto Representing the nested proto RepeatedKeyboardLayoutConfig
+ * @param keyboardLayoutConfiguration Class containing the fields for populating the
+ * KeyboardLayoutConfig proto
+ */
+ private static void addKeyboardLayoutConfigurationToProto(ProtoOutputStream proto,
+ KeyboardLayoutConfiguration keyboardLayoutConfiguration) {
+ // Start a new KeyboardLayoutConfig proto.
+ long keyboardLayoutConfigToken = proto.start(
+ RepeatedKeyboardLayoutConfig.KEYBOARD_LAYOUT_CONFIG);
+ proto.write(KeyboardLayoutConfig.KEYBOARD_LANGUAGE_TAG,
+ keyboardLayoutConfiguration.getKeyboardLanguageTag());
+ proto.write(KeyboardLayoutConfig.KEYBOARD_LAYOUT_TYPE,
+ keyboardLayoutConfiguration.getKeyboardLayoutType());
+ proto.write(KeyboardLayoutConfig.KEYBOARD_LAYOUT_NAME,
+ keyboardLayoutConfiguration.getKeyboardLayoutName());
+ proto.write(KeyboardLayoutConfig.LAYOUT_SELECTION_CRITERIA,
+ keyboardLayoutConfiguration.getLayoutSelectionCriteria());
+ proto.end(keyboardLayoutConfigToken);
+ }
+
+ /**
+ * Java class representing the proto KeyboardLayoutConfig defined in
+ * "frameworks/proto_logging/stats/atoms/input/input_extension_atoms.proto"
+ *
+ * @see com.android.os.input.KeyboardConfigured
+ */
+ public static class KeyboardLayoutConfiguration {
+ // KeyboardLayoutType in "frameworks/base/core/res/res/values/attrs.xml"
+ // contains mapping for enums to int
+ int mKeyboardLayoutType;
+ String mKeyboardLanguageTag;
+ KeyboardLayout mKeyboardLayout;
+ @LayoutSelectionCriteria int mLayoutSelectionCriteria;
+
+ @Retention(SOURCE)
+ @IntDef(prefix = { "LAYOUT_SELECTION_CRITERIA_" }, value = {
+ LAYOUT_SELECTION_CRITERIA_USER,
+ LAYOUT_SELECTION_CRITERIA_DEVICE,
+ LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD
+ })
+ public @interface LayoutSelectionCriteria {}
+
+ /** Manual selection by user */
+ public static final int LAYOUT_SELECTION_CRITERIA_USER = 0;
+
+ /** Auto-detection based on device provided language tag and layout type */
+ public static final int LAYOUT_SELECTION_CRITERIA_DEVICE = 1;
+
+ /** Auto-detection based on IME provided language tag and layout type */
+ public static final int LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD = 2;
+
+ KeyboardLayoutConfiguration(int keyboardLayoutType,
+ String keyboardLanguageTag,
+ KeyboardLayout keyboardLayout,
+ @LayoutSelectionCriteria int layoutSelectionCriteria) {
+ mKeyboardLayoutType = keyboardLayoutType;
+ mKeyboardLanguageTag = keyboardLanguageTag;
+ mKeyboardLayout = keyboardLayout;
+ mLayoutSelectionCriteria = layoutSelectionCriteria;
+ }
+ int getKeyboardLayoutType() {
+ return mKeyboardLayoutType;
+ }
+
+ String getKeyboardLanguageTag() {
+ return mKeyboardLanguageTag;
+ }
+
+ String getKeyboardLayoutName() {
+ return mKeyboardLayout.getLabel();
+ }
+
+ @LayoutSelectionCriteria int getLayoutSelectionCriteria() {
+ return mLayoutSelectionCriteria;
+ }
+ }
+}
+
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9c780bb..b1a289f 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -124,6 +124,7 @@
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiPlaybackClient;
import android.hardware.hdmi.HdmiPlaybackClient.OneTouchPlayCallback;
+import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.media.AudioManagerInternal;
import android.media.AudioSystem;
@@ -205,6 +206,7 @@
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.AccessibilityManagerInternal;
import com.android.server.ExtconStateObserver;
@@ -215,6 +217,7 @@
import com.android.server.UiThread;
import com.android.server.display.BrightnessUtils;
import com.android.server.input.InputManagerInternal;
+import com.android.server.input.KeyboardMetricsCollector;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.pm.UserManagerInternal;
import com.android.server.policy.KeyCombinationManager.TwoKeysCombinationRule;
@@ -536,10 +539,6 @@
int mShortPressOnSleepBehavior;
int mShortPressOnWindowBehavior;
int mPowerVolUpBehavior;
- int mShortPressOnStemPrimaryBehavior;
- int mDoublePressOnStemPrimaryBehavior;
- int mTriplePressOnStemPrimaryBehavior;
- int mLongPressOnStemPrimaryBehavior;
boolean mStylusButtonsEnabled = true;
boolean mHasSoftInput = false;
boolean mHapticTextHandleEnabled;
@@ -553,6 +552,12 @@
int mSearchKeyBehavior;
ComponentName mSearchKeyTargetActivity;
+ // Key Behavior - Stem Primary
+ private int mShortPressOnStemPrimaryBehavior;
+ private int mDoublePressOnStemPrimaryBehavior;
+ private int mTriplePressOnStemPrimaryBehavior;
+ private int mLongPressOnStemPrimaryBehavior;
+
private boolean mHandleVolumeKeysInWM;
private boolean mPendingKeyguardOccluded;
@@ -676,6 +681,7 @@
private static final int MSG_LAUNCH_ASSIST = 23;
private static final int MSG_RINGER_TOGGLE_CHORD = 24;
private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 25;
+ private static final int MSG_LOG_KEYBOARD_SYSTEM_EVENT = 26;
private class PolicyHandler extends Handler {
@Override
@@ -749,6 +755,9 @@
case MSG_SWITCH_KEYBOARD_LAYOUT:
handleSwitchKeyboardLayout(msg.arg1, msg.arg2);
break;
+ case MSG_LOG_KEYBOARD_SYSTEM_EVENT:
+ handleKeyboardSystemEvent(msg.arg2, (KeyEvent) msg.obj);
+ break;
}
}
}
@@ -1991,6 +2000,21 @@
Supplier<GlobalActions> getGlobalActionsFactory() {
return () -> new GlobalActions(mContext, mWindowManagerFuncs);
}
+
+ KeyguardServiceDelegate getKeyguardServiceDelegate() {
+ return new KeyguardServiceDelegate(mContext,
+ new StateCallback() {
+ @Override
+ public void onTrustedChanged() {
+ mWindowManagerFuncs.notifyKeyguardTrustedChanged();
+ }
+
+ @Override
+ public void onShowingChanged() {
+ mWindowManagerFuncs.onKeyguardShowingAndNotOccludedChanged();
+ }
+ });
+ }
}
/** {@inheritDoc} */
@@ -2248,18 +2272,7 @@
mKeyguardDrawnTimeout = mContext.getResources().getInteger(
com.android.internal.R.integer.config_keyguardDrawnTimeout);
- mKeyguardDelegate = new KeyguardServiceDelegate(mContext,
- new StateCallback() {
- @Override
- public void onTrustedChanged() {
- mWindowManagerFuncs.notifyKeyguardTrustedChanged();
- }
-
- @Override
- public void onShowingChanged() {
- mWindowManagerFuncs.onKeyguardShowingAndNotOccludedChanged();
- }
- });
+ mKeyguardDelegate = injector.getKeyguardServiceDelegate();
initKeyCombinationRules();
initSingleKeyGestureRules();
mSideFpsEventHandler = new SideFpsEventHandler(mContext, mHandler, mPowerManager);
@@ -2284,6 +2297,7 @@
cancelPendingScreenshotChordAction();
}
});
+
if (mHasFeatureWatch) {
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_POWER, KEYCODE_STEM_PRIMARY) {
@@ -2903,7 +2917,30 @@
WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
};
+ /**
+ * Log the keyboard shortcuts without blocking the current thread.
+ *
+ * We won't log keyboard events when the input device is null
+ * or when it is virtual.
+ */
+ private void handleKeyboardSystemEvent(int keyboardSystemEvent, KeyEvent event) {
+ final InputManager inputManager = mContext.getSystemService(InputManager.class);
+ final InputDevice inputDevice = inputManager != null
+ ? inputManager.getInputDevice(event.getDeviceId()) : null;
+ if (inputDevice != null && !inputDevice.isVirtual()) {
+ KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(
+ inputDevice, keyboardSystemEvent,
+ new int[]{event.getKeyCode()}, event.getMetaState());
+ }
+ }
+
+ private void logKeyboardSystemsEvent(KeyEvent event, int keyboardSystemEvent) {
+ mHandler.obtainMessage(MSG_LOG_KEYBOARD_SYSTEM_EVENT, 0, keyboardSystemEvent, event)
+ .sendToTarget();
+ }
+
// TODO(b/117479243): handle it in InputPolicy
+ // TODO (b/283241997): Add the remaining keyboard shortcut logging after refactoring
/** {@inheritDoc} */
@Override
public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
@@ -2957,6 +2994,8 @@
switch(keyCode) {
case KeyEvent.KEYCODE_HOME:
+ logKeyboardSystemsEvent(event, FrameworkStatsLog
+ .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__HOME);
return handleHomeShortcuts(displayId, focusedToken, event);
case KeyEvent.KEYCODE_MENU:
// Hijack modified menu keys for debugging features
@@ -2974,6 +3013,8 @@
case KeyEvent.KEYCODE_RECENT_APPS:
if (down && repeatCount == 0) {
showRecentApps(false /* triggeredFromAltTab */);
+ logKeyboardSystemsEvent(event, FrameworkStatsLog
+ .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RECENT_APPS);
}
return key_consumed;
case KeyEvent.KEYCODE_APP_SWITCH:
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 0ebec11..53a4752 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2926,15 +2926,6 @@
}
}
- // If the matching task is already in the adjacent task of the launch target. Adjust to use
- // the adjacent task as its launch target. So the existing task will be launched into the
- // closer one and won't be reparent redundantly.
- final Task adjacentTargetTask = mTargetRootTask.getAdjacentTask();
- if (adjacentTargetTask != null && intentActivity.isDescendantOf(adjacentTargetTask)
- && intentTask.isOnTop()) {
- mTargetRootTask = adjacentTargetTask;
- }
-
// If the target task is not in the front, then we need to bring it to the front...
// except... well, with SINGLE_TASK_LAUNCH it's not entirely clear. We'd like to have
// the same behavior as if a new instance was being started, which means not bringing it
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index da102d0..8dcb042 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -176,7 +176,7 @@
// TODO(b/266197298): Remove this by a more general protocol from the insets providers.
private static final boolean USE_CACHED_INSETS_FOR_DISPLAY_SWITCH =
- SystemProperties.getBoolean("persist.wm.debug.cached_insets_switch", false);
+ SystemProperties.getBoolean("persist.wm.debug.cached_insets_switch", true);
private final WindowManagerService mService;
private final Context mContext;
@@ -272,13 +272,13 @@
private WindowState mSystemUiControllingWindow;
// Candidate window to determine the color of navigation bar. The window needs to be top
- // fullscreen-app windows or dim layers that are intersecting with the window frame of status
- // bar.
+ // fullscreen-app windows or dim layers that are intersecting with the window frame of
+ // navigation bar.
private WindowState mNavBarColorWindowCandidate;
- // The window to determine opacity and background of translucent navigation bar. The window
- // needs to be opaque.
- private WindowState mNavBarBackgroundWindow;
+ // Candidate window to determine opacity and background of translucent navigation bar.
+ // The window frame must intersect the frame of navigation bar.
+ private WindowState mNavBarBackgroundWindowCandidate;
/**
* A collection of {@link AppearanceRegion} to indicate that which region of status bar applies
@@ -963,12 +963,6 @@
if (!win.mSession.mCanSetUnrestrictedGestureExclusion) {
attrs.privateFlags &= ~PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
}
-
- final InsetsSourceProvider provider = win.getControllableInsetProvider();
- if (provider != null && provider.getSource().insetsRoundedCornerFrame()
- != attrs.insetsRoundedCornerFrame) {
- provider.getSource().setInsetsRoundedCornerFrame(attrs.insetsRoundedCornerFrame);
- }
}
/**
@@ -1106,9 +1100,11 @@
} else {
overrideProviders = null;
}
- mDisplayContent.getInsetsStateController().getOrCreateSourceProvider(
- provider.getId(), provider.getType()).setWindowContainer(
- win, frameProvider, overrideProviders);
+ final InsetsSourceProvider sourceProvider = mDisplayContent
+ .getInsetsStateController().getOrCreateSourceProvider(provider.getId(),
+ provider.getType());
+ sourceProvider.getSource().setFlags(provider.getFlags());
+ sourceProvider.setWindowContainer(win, frameProvider, overrideProviders);
mInsetsSourceWindowsExceptIme.add(win);
}
}
@@ -1389,7 +1385,7 @@
mBottomGestureHost = null;
mTopFullscreenOpaqueWindowState = null;
mNavBarColorWindowCandidate = null;
- mNavBarBackgroundWindow = null;
+ mNavBarBackgroundWindowCandidate = null;
mStatusBarAppearanceRegionList.clear();
mLetterboxDetails.clear();
mStatusBarBackgroundWindows.clear();
@@ -1516,8 +1512,8 @@
mNavBarColorWindowCandidate = win;
addSystemBarColorApp(win);
}
- if (mNavBarBackgroundWindow == null) {
- mNavBarBackgroundWindow = win;
+ if (mNavBarBackgroundWindowCandidate == null) {
+ mNavBarBackgroundWindowCandidate = win;
}
}
@@ -1541,12 +1537,19 @@
}
if (isOverlappingWithNavBar(win) && mNavBarColorWindowCandidate == null) {
mNavBarColorWindowCandidate = win;
+ addSystemBarColorApp(win);
}
- } else if (appWindow && attached == null && mNavBarColorWindowCandidate == null
+ } else if (appWindow && attached == null
+ && (mNavBarColorWindowCandidate == null || mNavBarBackgroundWindowCandidate == null)
&& win.getFrame().contains(
getBarContentFrameForWindow(win, Type.navigationBars()))) {
- mNavBarColorWindowCandidate = win;
- addSystemBarColorApp(win);
+ if (mNavBarColorWindowCandidate == null) {
+ mNavBarColorWindowCandidate = win;
+ addSystemBarColorApp(win);
+ }
+ if (mNavBarBackgroundWindowCandidate == null) {
+ mNavBarBackgroundWindowCandidate = win;
+ }
}
}
@@ -2485,7 +2488,7 @@
return win.isFullyTransparentBarAllowed(getBarContentFrameForWindow(win, type));
}
- private boolean drawsBarBackground(WindowState win) {
+ private static boolean drawsBarBackground(WindowState win) {
if (win == null) {
return true;
}
@@ -2525,7 +2528,11 @@
*/
private int configureNavBarOpacity(int appearance, boolean multiWindowTaskVisible,
boolean freeformRootTaskVisible) {
- final boolean drawBackground = drawsBarBackground(mNavBarBackgroundWindow);
+ final WindowState navBackgroundWin = chooseNavigationBackgroundWindow(
+ mNavBarBackgroundWindowCandidate,
+ mDisplayContent.mInputMethodWindow,
+ mNavigationBarPosition);
+ final boolean drawBackground = navBackgroundWin != null;
if (mNavBarOpacityMode == NAV_BAR_FORCE_TRANSPARENT) {
if (drawBackground) {
@@ -2545,7 +2552,7 @@
}
}
- if (!isFullyTransparentAllowed(mNavBarBackgroundWindow, Type.navigationBars())) {
+ if (!isFullyTransparentAllowed(navBackgroundWin, Type.navigationBars())) {
appearance |= APPEARANCE_SEMI_TRANSPARENT_NAVIGATION_BARS;
}
@@ -2556,6 +2563,20 @@
return appearance & ~APPEARANCE_OPAQUE_NAVIGATION_BARS;
}
+ @VisibleForTesting
+ @Nullable
+ static WindowState chooseNavigationBackgroundWindow(WindowState candidate,
+ WindowState imeWindow, @NavigationBarPosition int navBarPosition) {
+ if (imeWindow != null && imeWindow.isVisible() && navBarPosition == NAV_BAR_BOTTOM
+ && drawsBarBackground(imeWindow)) {
+ return imeWindow;
+ }
+ if (drawsBarBackground(candidate)) {
+ return candidate;
+ }
+ return null;
+ }
+
private boolean isImmersiveMode(WindowState win) {
if (win == null) {
return false;
@@ -2728,9 +2749,9 @@
pw.print(prefix); pw.print("mNavBarColorWindowCandidate=");
pw.println(mNavBarColorWindowCandidate);
}
- if (mNavBarBackgroundWindow != null) {
- pw.print(prefix); pw.print("mNavBarBackgroundWindow=");
- pw.println(mNavBarBackgroundWindow);
+ if (mNavBarBackgroundWindowCandidate != null) {
+ pw.print(prefix); pw.print("mNavBarBackgroundWindowCandidate=");
+ pw.println(mNavBarBackgroundWindowCandidate);
}
if (mLastStatusBarAppearanceRegions != null) {
pw.print(prefix); pw.println("mLastStatusBarAppearanceRegions=");
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 2583514..4a3d0c1 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -181,7 +181,11 @@
}
} finally {
if (surface != null) {
- surface.release();
+ try (final SurfaceControl.Transaction transaction =
+ mService.mTransactionFactory.get()) {
+ transaction.remove(surface);
+ transaction.apply();
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index ddf96c5..698c9ab 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -440,8 +440,8 @@
return originalState;
}
- void onInsetsModified(InsetsControlTarget caller) {
- mStateController.onInsetsModified(caller);
+ void onRequestedVisibleTypesChanged(InsetsControlTarget caller) {
+ mStateController.onRequestedVisibleTypesChanged(caller);
checkAbortTransient(caller);
updateBarControlTarget(mFocusedWin);
}
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index b7eaf25..7f845e6 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -175,7 +175,7 @@
if (windowContainer == null) {
setServerVisible(false);
mSource.setVisibleFrame(null);
- mSource.setInsetsRoundedCornerFrame(false);
+ mSource.setFlags(0, 0xffffffff);
mSourceFrame.setEmpty();
} else {
mWindowContainer.getInsetsSourceProviders().put(mSource.getId(), this);
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 249ead0..addb521 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -190,7 +190,7 @@
}
}
- void onInsetsModified(InsetsControlTarget caller) {
+ void onRequestedVisibleTypesChanged(InsetsControlTarget caller) {
boolean changed = false;
for (int i = mProviders.size() - 1; i >= 0; i--) {
changed |= mProviders.valueAt(i).updateClientVisibility(caller);
@@ -352,7 +352,7 @@
// to the clients, so that the clients can change the current visibilities to the
// requested visibilities with animations.
for (int i = newControlTargets.size() - 1; i >= 0; i--) {
- onInsetsModified(newControlTargets.valueAt(i));
+ onRequestedVisibleTypesChanged(newControlTargets.valueAt(i));
}
newControlTargets.clear();
});
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index d83c861..c243988 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -1428,7 +1428,8 @@
for (int i = state.sourceSize() - 1; i >= 0; i--) {
final InsetsSource source = state.sourceAt(i);
if (source.getType() == WindowInsets.Type.navigationBars()
- && source.insetsRoundedCornerFrame() && source.isVisible()) {
+ && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)
+ && source.isVisible()) {
return source;
}
}
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index e47787e..9ef5ed0 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -1104,6 +1104,10 @@
// front unless overridden by the provided activity options
mTasks.remove(taskIndex);
mTasks.add(0, task);
+ if (taskIndex != 0) {
+ // Only notify when position changes
+ mTaskNotificationController.notifyTaskListUpdated();
+ }
if (DEBUG_RECENTS) {
Slog.d(TAG_RECENTS, "addRecent: moving to top " + task
@@ -1552,7 +1556,7 @@
task.affinity != null && task.affinity.equals(t.affinity);
final boolean sameIntent = intent != null && intent.filterEquals(trIntent);
boolean multiTasksAllowed = false;
- final int flags = intent.getFlags();
+ final int flags = intent != null ? intent.getFlags() : 0;
if ((flags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NEW_DOCUMENT)) != 0
&& (flags & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
multiTasksAllowed = true;
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 7b10c63..b49c5fb 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -687,11 +687,11 @@
@Override
public void updateRequestedVisibleTypes(IWindow window, @InsetsType int requestedVisibleTypes) {
synchronized (mService.mGlobalLock) {
- final WindowState windowState = mService.windowForClientLocked(this, window,
+ final WindowState win = mService.windowForClientLocked(this, window,
false /* throwOnError */);
- if (windowState != null) {
- windowState.setRequestedVisibleTypes(requestedVisibleTypes);
- windowState.getDisplayContent().getInsetsPolicy().onInsetsModified(windowState);
+ if (win != null) {
+ win.setRequestedVisibleTypes(requestedVisibleTypes);
+ win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win);
}
}
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index bb6f805..ce4f445 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -49,6 +49,7 @@
import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
import static android.view.SurfaceControl.METADATA_TASK_ID;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
@@ -1271,6 +1272,10 @@
if (isPersistable) {
mLastTimeMoved = System.currentTimeMillis();
}
+ if (toTop && inRecents) {
+ // If task is in recents, ensure it is at the top
+ mTaskSupervisor.mRecentTasks.add(this);
+ }
}
// Close up recents linked list.
@@ -2858,7 +2863,7 @@
getDisplayContent().getInsetsStateController().getRawInsetsState();
for (int i = state.sourceSize() - 1; i >= 0; i--) {
final InsetsSource source = state.sourceAt(i);
- if (source.insetsRoundedCornerFrame()) {
+ if (source.hasFlags(FLAG_INSETS_ROUNDED_CORNER)) {
animationBounds.inset(source.calculateVisibleInsets(animationBounds));
}
}
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 0152666..4bc4c26 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -4141,7 +4141,7 @@
getDisplayContent().getInsetsStateController().getSourceProviders();
for (int i = providers.size(); i >= 0; i--) {
final InsetsSourceProvider insetProvider = providers.valueAt(i);
- if (!insetProvider.getSource().insetsRoundedCornerFrame()) {
+ if (!insetProvider.getSource().hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) {
return;
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index e33c6f0..9c636ea 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -4537,7 +4537,8 @@
return;
}
dc.mRemoteInsetsControlTarget.setRequestedVisibleTypes(requestedVisibleTypes);
- dc.getInsetsStateController().onInsetsModified(dc.mRemoteInsetsControlTarget);
+ dc.getInsetsStateController().onRequestedVisibleTypesChanged(
+ dc.mRemoteInsetsControlTarget);
}
} finally {
Binder.restoreCallingIdentity(origId);
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index f0d718a..8587270 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -1596,8 +1596,8 @@
}
PointerIconStyle NativeInputManager::getDefaultStylusIconId() {
- // TODO: add resource for default stylus icon and change this
- return PointerIconStyle::TYPE_CROSSHAIR;
+ // Use the empty icon as the default pointer icon for a hovering stylus.
+ return PointerIconStyle::TYPE_NULL;
}
PointerIconStyle NativeInputManager::getCustomPointerIconId() {
@@ -2214,13 +2214,25 @@
jCapability |= env->GetStaticIntField(gLightClassInfo.clazz,
gLightClassInfo.lightCapabilityColorRgb);
}
+
+ ScopedLocalRef<jintArray> jPreferredBrightnessLevels{env};
+ if (!lightInfo.preferredBrightnessLevels.empty()) {
+ std::vector<int32_t> vec;
+ for (auto it : lightInfo.preferredBrightnessLevels) {
+ vec.push_back(ftl::to_underlying(it));
+ }
+ jPreferredBrightnessLevels.reset(env->NewIntArray(vec.size()));
+ env->SetIntArrayRegion(jPreferredBrightnessLevels.get(), 0, vec.size(), vec.data());
+ }
+
ScopedLocalRef<jobject> lightObj(env,
env->NewObject(gLightClassInfo.clazz,
gLightClassInfo.constructor,
static_cast<jint>(lightInfo.id),
env->NewStringUTF(lightInfo.name.c_str()),
static_cast<jint>(lightInfo.ordinal),
- jTypeId, jCapability));
+ jTypeId, jCapability,
+ jPreferredBrightnessLevels.get()));
// Add light object to list
env->CallBooleanMethod(jLights, gArrayListClassInfo.add, lightObj.get());
}
@@ -2846,7 +2858,7 @@
FIND_CLASS(gLightClassInfo.clazz, "android/hardware/lights/Light");
gLightClassInfo.clazz = jclass(env->NewGlobalRef(gLightClassInfo.clazz));
GET_METHOD_ID(gLightClassInfo.constructor, gLightClassInfo.clazz, "<init>",
- "(ILjava/lang/String;III)V");
+ "(ILjava/lang/String;III[I)V");
gLightClassInfo.clazz = jclass(env->NewGlobalRef(gLightClassInfo.clazz));
gLightClassInfo.lightTypeInput =
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index af84180..991248a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -3255,6 +3255,12 @@
Slog.d(TAG, "ContentCaptureService disabled because resource is not overlaid");
return;
}
+ if (!deviceHasConfigString(context, R.string.config_defaultContentProtectionService)) {
+ Slog.d(
+ TAG,
+ "ContentProtectionService disabled because resource is not overlaid,"
+ + " ContentCaptureService still enabled");
+ }
}
t.traceBegin("StartContentCaptureService");
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
index 8dc0ac6..efd82fa 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -813,7 +813,7 @@
any(HysteresisLevels.class),
any(HysteresisLevels.class),
eq(mContext),
- any(HighBrightnessModeController.class),
+ any(BrightnessRangeController.class),
any(BrightnessThrottler.class),
isNull(),
anyInt(),
@@ -1062,7 +1062,7 @@
HysteresisLevels screenBrightnessThresholds,
HysteresisLevels ambientBrightnessThresholdsIdle,
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
- HighBrightnessModeController hbmController,
+ BrightnessRangeController brightnessRangeController,
BrightnessThrottler brightnessThrottler,
BrightnessMappingStrategy idleModeBrightnessMapper,
int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
index 5c0810f..a93640b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -819,7 +819,7 @@
any(HysteresisLevels.class),
any(HysteresisLevels.class),
eq(mContext),
- any(HighBrightnessModeController.class),
+ any(BrightnessRangeController.class),
any(BrightnessThrottler.class),
isNull(),
anyInt(),
@@ -1038,7 +1038,7 @@
HysteresisLevels screenBrightnessThresholds,
HysteresisLevels ambientBrightnessThresholdsIdle,
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
- HighBrightnessModeController hbmController,
+ BrightnessRangeController brightnessRangeController,
BrightnessThrottler brightnessThrottler,
BrightnessMappingStrategy idleModeBrightnessMapper,
int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index cfeaf0b..2b9a227 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -28,6 +28,7 @@
"services.accessibility",
"services.appwidget",
"services.autofill",
+ "services.contentcapture",
"services.backup",
"services.companion",
"services.core",
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
index d5d06d3..046b01c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
@@ -16,8 +16,10 @@
package com.android.server.biometrics.sensors.face.aidl;
+import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -205,7 +207,9 @@
client.onAuthenticated(new Face("friendly", 1 /* faceId */, 2 /* deviceId */),
true /* authenticated */, new ArrayList<>());
- verify(mCancellationSignal).cancel();
+ verify(mCancellationSignal, never()).cancel();
+ verify(mClientMonitorCallbackConverter)
+ .onError(anyInt(), anyInt(), eq(BIOMETRIC_ERROR_CANCELED), anyInt());
}
private FaceAuthenticationClient createClient() throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index f8f40fe..c383a96 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -16,6 +16,8 @@
package com.android.server.biometrics.sensors.fingerprint.aidl;
+import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -399,7 +401,9 @@
mLooper.moveTimeForward(10);
mLooper.dispatchAll();
- verify(mCancellationSignal).cancel();
+ verify(mCancellationSignal, never()).cancel();
+ verify(mClientMonitorCallbackConverter)
+ .onError(anyInt(), anyInt(), eq(BIOMETRIC_ERROR_CANCELED), anyInt());
}
private FingerprintAuthenticationClient createClient() throws RemoteException {
diff --git a/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java
new file mode 100644
index 0000000..c872a11
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 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.contentcapture;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.ContentCaptureOptions;
+import android.content.Context;
+import android.content.pm.UserInfo;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Test for {@link ContentCaptureManagerService}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksServicesTests:com.android.server.contentcapture.ContentCaptureManagerServiceTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@SuppressWarnings("GuardedBy") // Service not really running, no need to expose locks
+public class ContentCaptureManagerServiceTest {
+
+ private static final int USER_ID = 1234;
+
+ private static final String PACKAGE_NAME = "com.test.package";
+
+ private static final Context sContext = ApplicationProvider.getApplicationContext();
+
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock private UserManagerInternal mMockUserManagerInternal;
+
+ private ContentCaptureManagerService mContentCaptureManagerService;
+
+ @Before
+ public void setup() {
+ when(mMockUserManagerInternal.getUserInfos()).thenReturn(new UserInfo[0]);
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
+ LocalServices.addService(UserManagerInternal.class, mMockUserManagerInternal);
+ mContentCaptureManagerService = new ContentCaptureManagerService(sContext);
+ }
+
+ @Test
+ public void getOptions_notAllowlisted() {
+ ContentCaptureOptions actual =
+ mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions(
+ USER_ID, PACKAGE_NAME);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void getOptions_allowlisted_contentCaptureReceiver() {
+ mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist(
+ USER_ID, ImmutableList.of(PACKAGE_NAME), /* components= */ null);
+
+ ContentCaptureOptions actual =
+ mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions(
+ USER_ID, PACKAGE_NAME);
+
+ assertThat(actual).isNotNull();
+ assertThat(actual.enableReceiver).isTrue();
+ assertThat(actual.contentProtectionOptions.enableReceiver).isFalse();
+ assertThat(actual.whitelistedComponents).isNull();
+ }
+
+ @Test
+ public void getOptions_allowlisted_bothReceivers() {
+ mContentCaptureManagerService.mDevCfgEnableContentProtectionReceiver = true;
+ mContentCaptureManagerService.mGlobalContentCaptureOptions.setWhitelist(
+ USER_ID, ImmutableList.of(PACKAGE_NAME), /* components= */ null);
+
+ ContentCaptureOptions actual =
+ mContentCaptureManagerService.mGlobalContentCaptureOptions.getOptions(
+ USER_ID, PACKAGE_NAME);
+
+ assertThat(actual).isNotNull();
+ assertThat(actual.enableReceiver).isTrue();
+ assertThat(actual.contentProtectionOptions.enableReceiver).isTrue();
+ assertThat(actual.whitelistedComponents).isNull();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionBlocklistManagerTest.java b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionBlocklistManagerTest.java
new file mode 100644
index 0000000..ba9956a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionBlocklistManagerTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.contentprotection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.pm.PackageInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test for {@link ContentProtectionBlocklistManager}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksServicesTests:
+ * com.android.server.contentprotection.ContentProtectionBlocklistManagerTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentProtectionBlocklistManagerTest {
+
+ private static final String FIRST_PACKAGE_NAME = "com.test.first.package.name";
+
+ private static final String SECOND_PACKAGE_NAME = "com.test.second.package.name";
+
+ private static final String UNLISTED_PACKAGE_NAME = "com.test.unlisted.package.name";
+
+ private static final String PACKAGE_NAME_BLOCKLIST_FILENAME =
+ "/product/etc/res/raw/content_protection/package_name_blocklist.txt";
+
+ private static final PackageInfo PACKAGE_INFO = new PackageInfo();
+
+ private static final List<String> LINES =
+ ImmutableList.of(FIRST_PACKAGE_NAME, SECOND_PACKAGE_NAME);
+
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock private ContentProtectionPackageManager mMockContentProtectionPackageManager;
+
+ private final List<String> mReadRawFiles = new ArrayList<>();
+
+ private ContentProtectionBlocklistManager mContentProtectionBlocklistManager;
+
+ @Before
+ public void setup() {
+ mContentProtectionBlocklistManager = new TestContentProtectionBlocklistManager();
+ }
+
+ @Test
+ public void isAllowed_blocklistNotLoaded() {
+ boolean actual = mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+
+ assertThat(actual).isFalse();
+ assertThat(mReadRawFiles).isEmpty();
+ verifyZeroInteractions(mMockContentProtectionPackageManager);
+ }
+
+ @Test
+ public void isAllowed_inBlocklist() {
+ mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+
+ boolean actual = mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+
+ assertThat(actual).isFalse();
+ verifyZeroInteractions(mMockContentProtectionPackageManager);
+ }
+
+ @Test
+ public void isAllowed_packageInfoNotFound() {
+ mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+ when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+ .thenReturn(null);
+
+ boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+ assertThat(actual).isFalse();
+ verify(mMockContentProtectionPackageManager, never())
+ .hasRequestedInternetPermissions(any());
+ verify(mMockContentProtectionPackageManager, never()).isSystemApp(any());
+ verify(mMockContentProtectionPackageManager, never()).isUpdatedSystemApp(any());
+ }
+
+ @Test
+ public void isAllowed_notRequestedInternet() {
+ mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+ when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+ .thenReturn(PACKAGE_INFO);
+ when(mMockContentProtectionPackageManager.hasRequestedInternetPermissions(PACKAGE_INFO))
+ .thenReturn(false);
+
+ boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+ assertThat(actual).isFalse();
+ verify(mMockContentProtectionPackageManager, never()).isSystemApp(any());
+ verify(mMockContentProtectionPackageManager, never()).isUpdatedSystemApp(any());
+ }
+
+ @Test
+ public void isAllowed_systemApp() {
+ mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+ when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+ .thenReturn(PACKAGE_INFO);
+ when(mMockContentProtectionPackageManager.hasRequestedInternetPermissions(PACKAGE_INFO))
+ .thenReturn(true);
+ when(mMockContentProtectionPackageManager.isSystemApp(PACKAGE_INFO)).thenReturn(true);
+
+ boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+ assertThat(actual).isFalse();
+ verify(mMockContentProtectionPackageManager, never()).isUpdatedSystemApp(any());
+ }
+
+ @Test
+ public void isAllowed_updatedSystemApp() {
+ mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+ when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+ .thenReturn(PACKAGE_INFO);
+ when(mMockContentProtectionPackageManager.hasRequestedInternetPermissions(PACKAGE_INFO))
+ .thenReturn(true);
+ when(mMockContentProtectionPackageManager.isSystemApp(PACKAGE_INFO)).thenReturn(true);
+ when(mMockContentProtectionPackageManager.isUpdatedSystemApp(PACKAGE_INFO))
+ .thenReturn(true);
+
+ boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+ assertThat(actual).isFalse();
+ }
+
+ @Test
+ public void isAllowed_allowed() {
+ mContentProtectionBlocklistManager.updateBlocklist(LINES.size());
+ when(mMockContentProtectionPackageManager.getPackageInfo(UNLISTED_PACKAGE_NAME))
+ .thenReturn(PACKAGE_INFO);
+ when(mMockContentProtectionPackageManager.hasRequestedInternetPermissions(PACKAGE_INFO))
+ .thenReturn(true);
+ when(mMockContentProtectionPackageManager.isSystemApp(PACKAGE_INFO)).thenReturn(false);
+ when(mMockContentProtectionPackageManager.isUpdatedSystemApp(PACKAGE_INFO))
+ .thenReturn(false);
+
+ boolean actual = mContentProtectionBlocklistManager.isAllowed(UNLISTED_PACKAGE_NAME);
+
+ assertThat(actual).isTrue();
+ }
+
+ @Test
+ public void updateBlocklist_negativeSize() {
+ mContentProtectionBlocklistManager.updateBlocklist(/* blocklistSize= */ -1);
+ assertThat(mReadRawFiles).isEmpty();
+
+ mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+ verify(mMockContentProtectionPackageManager).getPackageInfo(FIRST_PACKAGE_NAME);
+ }
+
+ @Test
+ public void updateBlocklist_zeroSize() {
+ mContentProtectionBlocklistManager.updateBlocklist(/* blocklistSize= */ 0);
+ assertThat(mReadRawFiles).isEmpty();
+
+ mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+ verify(mMockContentProtectionPackageManager).getPackageInfo(FIRST_PACKAGE_NAME);
+ }
+
+ @Test
+ public void updateBlocklist_positiveSize_belowTotal() {
+ mContentProtectionBlocklistManager.updateBlocklist(/* blocklistSize= */ 1);
+ assertThat(mReadRawFiles).containsExactly(PACKAGE_NAME_BLOCKLIST_FILENAME);
+
+ mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+ mContentProtectionBlocklistManager.isAllowed(SECOND_PACKAGE_NAME);
+
+ verify(mMockContentProtectionPackageManager, never()).getPackageInfo(FIRST_PACKAGE_NAME);
+ verify(mMockContentProtectionPackageManager).getPackageInfo(SECOND_PACKAGE_NAME);
+ }
+
+ @Test
+ public void updateBlocklist_positiveSize_aboveTotal() {
+ mContentProtectionBlocklistManager.updateBlocklist(LINES.size() + 1);
+ assertThat(mReadRawFiles).containsExactly(PACKAGE_NAME_BLOCKLIST_FILENAME);
+
+ mContentProtectionBlocklistManager.isAllowed(FIRST_PACKAGE_NAME);
+ mContentProtectionBlocklistManager.isAllowed(SECOND_PACKAGE_NAME);
+
+ verify(mMockContentProtectionPackageManager, never()).getPackageInfo(FIRST_PACKAGE_NAME);
+ verify(mMockContentProtectionPackageManager, never()).getPackageInfo(SECOND_PACKAGE_NAME);
+ }
+
+ private final class TestContentProtectionBlocklistManager
+ extends ContentProtectionBlocklistManager {
+
+ TestContentProtectionBlocklistManager() {
+ super(mMockContentProtectionPackageManager);
+ }
+
+ @Override
+ protected List<String> readLinesFromRawFile(@NonNull String filename) {
+ mReadRawFiles.add(filename);
+ return LINES;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java
new file mode 100644
index 0000000..7d45ea4
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionPackageManagerTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.contentprotection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.Manifest.permission;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManager.PackageInfoFlags;
+import android.testing.TestableContext;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Test for {@link ContentProtectionPackageManager}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksServicesTests:com.android.server.contentprotection.ContentProtectionPackageManagerTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentProtectionPackageManagerTest {
+ private static final String PACKAGE_NAME = "PACKAGE_NAME";
+
+ private static final PackageInfo EMPTY_PACKAGE_INFO = new PackageInfo();
+
+ private static final PackageInfo SYSTEM_APP_PACKAGE_INFO = createSystemAppPackageInfo();
+
+ private static final PackageInfo UPDATED_SYSTEM_APP_PACKAGE_INFO =
+ createUpdatedSystemAppPackageInfo();
+
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Rule
+ public final TestableContext mContext =
+ new TestableContext(ApplicationProvider.getApplicationContext());
+
+ @Mock private PackageManager mMockPackageManager;
+
+ private ContentProtectionPackageManager mContentProtectionPackageManager;
+
+ @Before
+ public void setup() {
+ mContext.setMockPackageManager(mMockPackageManager);
+ mContentProtectionPackageManager = new ContentProtectionPackageManager(mContext);
+ }
+
+ @Test
+ public void getPackageInfo_found() throws Exception {
+ PackageInfo expected = createPackageInfo(/* flags= */ 0);
+ when(mMockPackageManager.getPackageInfo(eq(PACKAGE_NAME), any(PackageInfoFlags.class)))
+ .thenReturn(expected);
+
+ PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void getPackageInfo_notFound() throws Exception {
+ when(mMockPackageManager.getPackageInfo(eq(PACKAGE_NAME), any(PackageInfoFlags.class)))
+ .thenThrow(new NameNotFoundException());
+
+ PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void getPackageInfo_null() {
+ PackageInfo actual = mContentProtectionPackageManager.getPackageInfo(PACKAGE_NAME);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void isSystemApp_true() {
+ boolean actual = mContentProtectionPackageManager.isSystemApp(SYSTEM_APP_PACKAGE_INFO);
+
+ assertThat(actual).isTrue();
+ }
+
+ @Test
+ public void isSystemApp_false() {
+ boolean actual =
+ mContentProtectionPackageManager.isSystemApp(UPDATED_SYSTEM_APP_PACKAGE_INFO);
+
+ assertThat(actual).isFalse();
+ }
+
+ @Test
+ public void isSystemApp_noApplicationInfo() {
+ boolean actual = mContentProtectionPackageManager.isSystemApp(EMPTY_PACKAGE_INFO);
+
+ assertThat(actual).isFalse();
+ }
+
+ @Test
+ public void isUpdatedSystemApp_true() {
+ boolean actual =
+ mContentProtectionPackageManager.isUpdatedSystemApp(
+ UPDATED_SYSTEM_APP_PACKAGE_INFO);
+
+ assertThat(actual).isTrue();
+ }
+
+ @Test
+ public void isUpdatedSystemApp_false() {
+ boolean actual =
+ mContentProtectionPackageManager.isUpdatedSystemApp(SYSTEM_APP_PACKAGE_INFO);
+
+ assertThat(actual).isFalse();
+ }
+
+ @Test
+ public void isUpdatedSystemApp_noApplicationInfo() {
+ boolean actual = mContentProtectionPackageManager.isUpdatedSystemApp(EMPTY_PACKAGE_INFO);
+
+ assertThat(actual).isFalse();
+ }
+
+ @Test
+ public void hasRequestedInternetPermissions_true() {
+ PackageInfo packageInfo = createPackageInfo(new String[] {permission.INTERNET});
+
+ boolean actual =
+ mContentProtectionPackageManager.hasRequestedInternetPermissions(packageInfo);
+
+ assertThat(actual).isTrue();
+ }
+
+ @Test
+ public void hasRequestedInternetPermissions_false() {
+ PackageInfo packageInfo = createPackageInfo(new String[] {permission.ACCESS_FINE_LOCATION});
+
+ boolean actual =
+ mContentProtectionPackageManager.hasRequestedInternetPermissions(packageInfo);
+
+ assertThat(actual).isFalse();
+ }
+
+ @Test
+ public void hasRequestedInternetPermissions_noRequestedPermissions() {
+ boolean actual =
+ mContentProtectionPackageManager.hasRequestedInternetPermissions(
+ EMPTY_PACKAGE_INFO);
+
+ assertThat(actual).isFalse();
+ }
+
+ private static PackageInfo createSystemAppPackageInfo() {
+ return createPackageInfo(ApplicationInfo.FLAG_SYSTEM);
+ }
+
+ private static PackageInfo createUpdatedSystemAppPackageInfo() {
+ return createPackageInfo(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP);
+ }
+
+ private static PackageInfo createPackageInfo(int flags) {
+ return createPackageInfo(flags, /* requestedPermissions= */ new String[0]);
+ }
+
+ private static PackageInfo createPackageInfo(String[] requestedPermissions) {
+ return createPackageInfo(/* flags= */ 0, requestedPermissions);
+ }
+
+ private static PackageInfo createPackageInfo(int flags, String[] requestedPermissions) {
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = PACKAGE_NAME;
+ packageInfo.applicationInfo = new ApplicationInfo();
+ packageInfo.applicationInfo.packageName = PACKAGE_NAME;
+ packageInfo.applicationInfo.flags = flags;
+ packageInfo.requestedPermissions = requestedPermissions;
+ return packageInfo;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index 962e867..a6acd60 100644
--- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -85,7 +85,7 @@
@Mock HysteresisLevels mAmbientBrightnessThresholdsIdle;
@Mock HysteresisLevels mScreenBrightnessThresholdsIdle;
@Mock Handler mNoOpHandler;
- @Mock HighBrightnessModeController mHbmController;
+ @Mock BrightnessRangeController mBrightnessRangeController;
@Mock BrightnessThrottler mBrightnessThrottler;
@Before
@@ -134,12 +134,15 @@
DARKENING_LIGHT_DEBOUNCE_CONFIG, RESET_AMBIENT_LUX_AFTER_WARMUP_CONFIG,
mAmbientBrightnessThresholds, mScreenBrightnessThresholds,
mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
- mContext, mHbmController, mBrightnessThrottler, mIdleBrightnessMappingStrategy,
- AMBIENT_LIGHT_HORIZON_SHORT, AMBIENT_LIGHT_HORIZON_LONG, userLux, userBrightness
+ mContext, mBrightnessRangeController, mBrightnessThrottler,
+ mIdleBrightnessMappingStrategy, AMBIENT_LIGHT_HORIZON_SHORT,
+ AMBIENT_LIGHT_HORIZON_LONG, userLux, userBrightness
);
- when(mHbmController.getCurrentBrightnessMax()).thenReturn(BRIGHTNESS_MAX_FLOAT);
- when(mHbmController.getCurrentBrightnessMin()).thenReturn(BRIGHTNESS_MIN_FLOAT);
+ when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
+ BRIGHTNESS_MAX_FLOAT);
+ when(mBrightnessRangeController.getCurrentBrightnessMin()).thenReturn(
+ BRIGHTNESS_MIN_FLOAT);
// Disable brightness throttling by default. Individual tests can enable it as needed.
when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
when(mBrightnessThrottler.isThrottled()).thenReturn(false);
diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
index 817b245..642f54c 100644
--- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -60,6 +60,114 @@
}
@Test
+ public void testLoadBrightness() {
+ final String uniqueDisplayId = "test:123";
+ final DisplayDevice testDisplayDevice = new DisplayDevice(
+ null, null, uniqueDisplayId, null) {
+ @Override
+ public boolean hasStableUniqueId() {
+ return true;
+ }
+
+ @Override
+ public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+ return null;
+ }
+ };
+
+ String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+ + "<display-manager-state>\n"
+ + " <display-states>\n"
+ + " <display unique-id=\"test:123\">\n"
+ + " <brightness-value user-serial=\"1\">0.1</brightness-value>\n"
+ + " <brightness-value user-serial=\"2\">0.2</brightness-value>\n"
+ + " </display>\n"
+ + " </display-states>\n"
+ + "</display-manager-state>\n";
+
+ InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
+ mInjector.setReadStream(is);
+ mDataStore.loadIfNeeded();
+
+ float brightness = mDataStore.getBrightness(testDisplayDevice, 1);
+ assertEquals(0.1, brightness, 0.01);
+
+ brightness = mDataStore.getBrightness(testDisplayDevice, 2);
+ assertEquals(0.2, brightness, 0.01);
+ }
+
+ @Test
+ public void testSetBrightness_brightnessTagWithNoUserId_updatesToBrightnessTagWithUserId() {
+ final String uniqueDisplayId = "test:123";
+ final DisplayDevice testDisplayDevice =
+ new DisplayDevice(null, null, uniqueDisplayId, null) {
+ @Override
+ public boolean hasStableUniqueId() {
+ return true;
+ }
+
+ @Override
+ public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
+ return null;
+ }
+ };
+
+ String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+ + "<display-manager-state>\n"
+ + " <display-states>\n"
+ + " <color-mode>0</color-mode>\n"
+ + " <display unique-id=\"test:123\">\n"
+ + " <brightness-value>0.5</brightness-value>\n"
+ + " </display>\n"
+ + " </display-states>\n"
+ + "</display-manager-state>\n";
+
+ InputStream is = new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8));
+ mInjector.setReadStream(is);
+ mDataStore.loadIfNeeded();
+
+ float user1Brightness = mDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */);
+ float user2Brightness = mDataStore.getBrightness(testDisplayDevice, 2 /* userSerial */);
+ assertEquals(0.5, user1Brightness, 0.01);
+ assertEquals(0.5, user2Brightness, 0.01);
+
+ // Override the value for user 2. Default user must have been removed.
+ mDataStore.setBrightness(testDisplayDevice, 0.2f, 2 /* userSerial */ /* brightness*/);
+
+ user1Brightness = mDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */);
+ user2Brightness = mDataStore.getBrightness(testDisplayDevice, 2 /* userSerial */);
+ assertTrue(Float.isNaN(user1Brightness));
+ assertEquals(0.2f, user2Brightness, 0.01);
+
+ // Override the value for user 1. User-specific brightness values should co-exist.
+ mDataStore.setBrightness(testDisplayDevice, 0.1f, 1 /* userSerial */ /* brightness*/);
+ user1Brightness = mDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */);
+ user2Brightness = mDataStore.getBrightness(testDisplayDevice, 2 /* userSerial */);
+ assertEquals(0.1f, user1Brightness, 0.01);
+ assertEquals(0.2f, user2Brightness, 0.01);
+
+ // Validate saveIfNeeded writes user-specific brightnes.
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ mInjector.setWriteStream(baos);
+ mDataStore.saveIfNeeded();
+ mTestLooper.dispatchAll();
+ assertTrue(mInjector.wasWriteSuccessful());
+ TestInjector newInjector = new TestInjector();
+ PersistentDataStore newDataStore = new PersistentDataStore(newInjector);
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ newInjector.setReadStream(bais);
+ newDataStore.loadIfNeeded();
+
+ user1Brightness = newDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */);
+ user2Brightness = newDataStore.getBrightness(testDisplayDevice, 2 /* userSerial */);
+ float unknownUserBrightness =
+ newDataStore.getBrightness(testDisplayDevice, 999 /* userSerial */);
+ assertEquals(0.1f, user1Brightness, 0.01);
+ assertEquals(0.2f, user2Brightness, 0.01);
+ assertTrue(Float.isNaN(unknownUserBrightness));
+ }
+
+ @Test
public void testLoadingBrightnessConfigurations() {
String contents = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+ "<display-manager-state>\n"
@@ -374,7 +482,7 @@
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
newInjector.setReadStream(bais);
newDataStore.loadIfNeeded();
- assertTrue(Float.isNaN(mDataStore.getBrightness(testDisplayDevice)));
+ assertTrue(Float.isNaN(mDataStore.getBrightness(testDisplayDevice, 1 /* userSerial */)));
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
index e6d3bbc..c4f4838 100644
--- a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -247,6 +248,7 @@
0.0f);
verify(mBrightnessChangeExecutor).execute(mOnBrightnessChangeRunnable);
verify(mBrightnessSetting).setBrightness(brightnessValue);
+ verify(mBrightnessSetting).setUserSerial(anyInt());
// Does nothing if the value is invalid
mDisplayBrightnessController.updateScreenBrightnessSetting(Float.NaN);
@@ -358,4 +360,28 @@
verify(mBrightnessSetting, never()).getBrightnessNitsForDefaultDisplay();
verify(mBrightnessSetting, never()).setBrightness(brightness);
}
+
+ @Test
+ public void testChangeBrightnessNitsWhenUserChanges() {
+ float brightnessValue1 = 0.3f;
+ float nits1 = 200f;
+ float brightnessValue2 = 0.5f;
+ float nits2 = 300f;
+ AutomaticBrightnessController automaticBrightnessController =
+ mock(AutomaticBrightnessController.class);
+ when(automaticBrightnessController.convertToNits(brightnessValue1)).thenReturn(nits1);
+ when(automaticBrightnessController.convertToNits(brightnessValue2)).thenReturn(nits2);
+ mDisplayBrightnessController.setAutomaticBrightnessController(
+ automaticBrightnessController);
+
+ mDisplayBrightnessController.setBrightness(brightnessValue1, 1 /* user-serial */);
+ verify(mBrightnessSetting).setUserSerial(1);
+ verify(mBrightnessSetting).setBrightness(brightnessValue1);
+ verify(mBrightnessSetting).setBrightnessNitsForDefaultDisplay(nits1);
+
+ mDisplayBrightnessController.setBrightness(brightnessValue2, 2 /* user-serial */);
+ verify(mBrightnessSetting).setUserSerial(2);
+ verify(mBrightnessSetting).setBrightness(brightnessValue2);
+ verify(mBrightnessSetting).setBrightnessNitsForDefaultDisplay(nits2);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
index 64c05dc..ef15ccb 100644
--- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
@@ -16,6 +16,7 @@
package com.android.server.input
+import android.animation.ValueAnimator
import android.content.Context
import android.content.ContextWrapper
import android.graphics.Color
@@ -29,8 +30,10 @@
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import android.view.InputDevice
+import androidx.test.annotation.UiThreadTest
import androidx.test.core.app.ApplicationProvider
-import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL
+import com.android.server.input.KeyboardBacklightController.DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL
+import com.android.server.input.KeyboardBacklightController.MAX_BRIGHTNESS_CHANGE_STEPS
import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS
import org.junit.After
import org.junit.Assert.assertEquals
@@ -63,12 +66,20 @@
.build()
private fun createLight(lightId: Int, lightType: Int): Light =
+ createLight(
+ lightId,
+ lightType,
+ null
+ )
+
+private fun createLight(lightId: Int, lightType: Int, suggestedBrightnessLevels: IntArray?): Light =
Light(
lightId,
"Light $lightId",
1,
lightType,
- Light.LIGHT_CAPABILITY_BRIGHTNESS
+ Light.LIGHT_CAPABILITY_BRIGHTNESS,
+ suggestedBrightnessLevels
)
/**
* Tests for {@link KeyboardBacklightController}.
@@ -99,6 +110,7 @@
private var lightColorMap: HashMap<Int, Int> = HashMap()
private var lastBacklightState: KeyboardBacklightState? = null
private var sysfsNodeChanges = 0
+ private var lastAnimationValues = IntArray(2)
@Before
fun setup() {
@@ -115,8 +127,8 @@
override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
})
testLooper = TestLooper()
- keyboardBacklightController =
- KeyboardBacklightController(context, native, dataStore, testLooper.looper)
+ keyboardBacklightController = KeyboardBacklightController(context, native, dataStore,
+ testLooper.looper, FakeAnimatorFactory())
InputManagerGlobal.resetInstance(iInputManager)
val inputManager = InputManager(context)
`when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
@@ -125,6 +137,10 @@
val args = it.arguments
lightColorMap.put(args[1] as Int, args[2] as Int)
}
+ `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer {
+ val args = it.arguments
+ lightColorMap.getOrDefault(args[1] as Int, 0)
+ }
lightColorMap.clear()
`when`(native.sysfsNodeChanged(any())).then {
sysfsNodeChanges++
@@ -138,271 +154,225 @@
@Test
fun testKeyboardBacklightIncrementDecrement() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
- incrementKeyboardBacklight(DEVICE_ID)
- assertEquals(
- "Light value for level $level mismatched",
- Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
- assertEquals(
- "Light value for level $level must be correctly stored in the datastore",
- BRIGHTNESS_VALUE_FOR_LEVEL[level],
- dataStore.getKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID
- ).asInt
- )
+ assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+ DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL)
}
-
- // Increment above max level
- incrementKeyboardBacklight(DEVICE_ID)
- assertEquals(
- "Light value for max level mismatched",
- Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
- assertEquals(
- "Light value for max level must be correctly stored in the datastore",
- MAX_BRIGHTNESS,
- dataStore.getKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID
- ).asInt
- )
-
- for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) {
- decrementKeyboardBacklight(DEVICE_ID)
- assertEquals(
- "Light value for level $level mismatched",
- Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
- assertEquals(
- "Light value for level $level must be correctly stored in the datastore",
- BRIGHTNESS_VALUE_FOR_LEVEL[level],
- dataStore.getKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID
- ).asInt
- )
- }
-
- // Decrement below min level
- decrementKeyboardBacklight(DEVICE_ID)
- assertEquals(
- "Light value for min level mismatched",
- Color.argb(0, 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
- assertEquals(
- "Light value for min level must be correctly stored in the datastore",
- 0,
- dataStore.getKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID
- ).asInt
- )
}
@Test
fun testKeyboardWithoutBacklight() {
- val keyboardWithoutBacklight = createKeyboard(DEVICE_ID)
- val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight))
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use {
+ val keyboardWithoutBacklight = createKeyboard(DEVICE_ID)
+ val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight))
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- incrementKeyboardBacklight(DEVICE_ID)
- assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty())
+ incrementKeyboardBacklight(DEVICE_ID)
+ assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty())
+ }
}
@Test
fun testKeyboardWithMultipleLight() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(
- listOf(
- keyboardBacklight,
- keyboardInputLight
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(
+ listOf(
+ keyboardBacklight,
+ keyboardInputLight
+ )
)
- )
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- incrementKeyboardBacklight(DEVICE_ID)
- assertEquals("Only keyboard backlights should change", 1, lightColorMap.size)
- assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID])
- assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID])
+ incrementKeyboardBacklight(DEVICE_ID)
+ assertEquals("Only keyboard backlights should change", 1, lightColorMap.size)
+ assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID])
+ assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID])
+ }
}
@Test
fun testRestoreBacklightOnInputDeviceAdded() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
- dataStore.setKeyboardBacklightBrightness(
+ for (level in 1 until DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size) {
+ dataStore.setKeyboardBacklightBrightness(
keyboardWithBacklight.descriptor,
LIGHT_ID,
- BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1
+ DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1
+ )
+
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchNext()
+ assertEquals(
+ "Keyboard backlight level should be restored to the level saved in the " +
+ "data store",
+ Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
+ lightColorMap[LIGHT_ID]
+ )
+ keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID)
+ }
+ }
+ }
+
+ @Test
+ fun testRestoreBacklightOnInputDeviceChanged() {
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ dataStore.setKeyboardBacklightBrightness(
+ keyboardWithBacklight.descriptor,
+ LIGHT_ID,
+ MAX_BRIGHTNESS
+ )
+
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchNext()
+ assertTrue(
+ "Keyboard backlight should not be changed until its added",
+ lightColorMap.isEmpty()
+ )
+
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ keyboardBacklightController.onInputDeviceChanged(DEVICE_ID)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchNext()
+ assertEquals(
+ "Keyboard backlight level should be restored to the level saved in the data store",
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+ lightColorMap[LIGHT_ID]
+ )
+ }
+ }
+
+ @Test
+ fun testKeyboardBacklight_registerUnregisterListener() {
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ val maxLevel = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size - 1
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+ // Register backlight listener
+ val listener = KeyboardBacklightListener()
+ keyboardBacklightController.registerKeyboardBacklightListener(listener, 0)
+
+ lastBacklightState = null
+ keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
+ testLooper.dispatchNext()
+
+ assertEquals(
+ "Backlight state device Id should be $DEVICE_ID",
+ DEVICE_ID,
+ lastBacklightState!!.deviceId
+ )
+ assertEquals(
+ "Backlight state brightnessLevel should be 1",
+ 1,
+ lastBacklightState!!.brightnessLevel
+ )
+ assertEquals(
+ "Backlight state maxBrightnessLevel should be $maxLevel",
+ maxLevel,
+ lastBacklightState!!.maxBrightnessLevel
+ )
+ assertEquals(
+ "Backlight state isTriggeredByKeyPress should be true",
+ true,
+ lastBacklightState!!.isTriggeredByKeyPress
+ )
+
+ // Unregister listener
+ keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
+
+ lastBacklightState = null
+ incrementKeyboardBacklight(DEVICE_ID)
+
+ assertNull("Listener should not receive any updates", lastBacklightState)
+ }
+ }
+
+ @Test
+ fun testKeyboardBacklight_userActivity() {
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ dataStore.setKeyboardBacklightBrightness(
+ keyboardWithBacklight.descriptor,
+ LIGHT_ID,
+ MAX_BRIGHTNESS
)
keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
keyboardBacklightController.notifyUserActivity()
testLooper.dispatchNext()
assertEquals(
- "Keyboard backlight level should be restored to the level saved in the data " +
- "store",
- Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
- lightColorMap[LIGHT_ID]
+ "Keyboard backlight level should be restored to the level saved in the data store",
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+ lightColorMap[LIGHT_ID]
)
- keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID)
+
+ testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000)
+ testLooper.dispatchNext()
+ assertEquals(
+ "Keyboard backlight level should be turned off after inactivity",
+ 0,
+ lightColorMap[LIGHT_ID]
+ )
}
}
@Test
- fun testRestoreBacklightOnInputDeviceChanged() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- dataStore.setKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID,
- MAX_BRIGHTNESS
- )
-
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- keyboardBacklightController.notifyUserActivity()
- testLooper.dispatchNext()
- assertTrue(
- "Keyboard backlight should not be changed until its added",
- lightColorMap.isEmpty()
- )
-
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- keyboardBacklightController.onInputDeviceChanged(DEVICE_ID)
- keyboardBacklightController.notifyUserActivity()
- testLooper.dispatchNext()
- assertEquals(
- "Keyboard backlight level should be restored to the level saved in the data store",
- Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
- }
-
- @Test
- fun testKeyboardBacklight_registerUnregisterListener() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
-
- // Register backlight listener
- val listener = KeyboardBacklightListener()
- keyboardBacklightController.registerKeyboardBacklightListener(listener, 0)
-
- lastBacklightState = null
- keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID)
- testLooper.dispatchNext()
-
- assertEquals(
- "Backlight state device Id should be $DEVICE_ID",
- DEVICE_ID,
- lastBacklightState!!.deviceId
- )
- assertEquals(
- "Backlight state brightnessLevel should be " + 1,
- 1,
- lastBacklightState!!.brightnessLevel
- )
- assertEquals(
- "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
- (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1),
- lastBacklightState!!.maxBrightnessLevel
- )
- assertEquals(
- "Backlight state isTriggeredByKeyPress should be true",
- true,
- lastBacklightState!!.isTriggeredByKeyPress
- )
-
- // Unregister listener
- keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0)
-
- lastBacklightState = null
- incrementKeyboardBacklight(DEVICE_ID)
-
- assertNull("Listener should not receive any updates", lastBacklightState)
- }
-
- @Test
- fun testKeyboardBacklight_userActivity() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- dataStore.setKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID,
- MAX_BRIGHTNESS
- )
-
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- keyboardBacklightController.notifyUserActivity()
- testLooper.dispatchNext()
- assertEquals(
- "Keyboard backlight level should be restored to the level saved in the data store",
- Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
-
- testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000)
- testLooper.dispatchNext()
- assertEquals(
- "Keyboard backlight level should be turned off after inactivity",
- 0,
- lightColorMap[LIGHT_ID]
- )
- }
-
- @Test
fun testKeyboardBacklight_displayOnOff() {
- val keyboardWithBacklight = createKeyboard(DEVICE_ID)
- val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
- `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
- `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- dataStore.setKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID,
- MAX_BRIGHTNESS
- )
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = false).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ dataStore.setKeyboardBacklightBrightness(
+ keyboardWithBacklight.descriptor,
+ LIGHT_ID,
+ MAX_BRIGHTNESS
+ )
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */)
- assertEquals(
- "Keyboard backlight level should be restored to the level saved in the data " +
- "store when display turned on",
- Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */)
+ assertEquals(
+ "Keyboard backlight level should be restored to the level saved in the data " +
+ "store when display turned on",
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+ lightColorMap[LIGHT_ID]
+ )
- keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */)
- assertEquals(
- "Keyboard backlight level should be turned off after display is turned off",
- 0,
- lightColorMap[LIGHT_ID]
- )
+ keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */)
+ assertEquals(
+ "Keyboard backlight level should be turned off after display is turned off",
+ 0,
+ lightColorMap[LIGHT_ID]
+ )
+ }
}
@Test
@@ -463,6 +433,158 @@
)
}
+ @Test
+ @UiThreadTest
+ fun testKeyboardBacklightAnimation_onChangeLevels() {
+ KeyboardBacklightFlags(animationEnabled = true, customLevelsEnabled = false).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+ incrementKeyboardBacklight(DEVICE_ID)
+ assertEquals(
+ "Should start animation from level 0",
+ DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[0],
+ lastAnimationValues[0]
+ )
+ assertEquals(
+ "Should start animation to level 1",
+ DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1],
+ lastAnimationValues[1]
+ )
+ }
+ }
+
+ @Test
+ fun testKeyboardBacklightPreferredLevels() {
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = true).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val suggestedLevels = intArrayOf(0, 22, 63, 135, 196, 255)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+ suggestedLevels)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+ assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+ suggestedLevels)
+ }
+ }
+
+ @Test
+ fun testKeyboardBacklightPreferredLevels_moreThanMax_shouldUseDefault() {
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = true).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val suggestedLevels = IntArray(MAX_BRIGHTNESS_CHANGE_STEPS + 1) { 10 * (it + 1) }
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+ suggestedLevels)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+ assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+ DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL)
+ }
+ }
+
+ @Test
+ fun testKeyboardBacklightPreferredLevels_mustHaveZeroAndMaxBrightnessAsBounds() {
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = true).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val suggestedLevels = intArrayOf(22, 63, 135, 196)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+ suggestedLevels)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+ // Framework will add the lowest and maximum levels if not provided via config
+ assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+ intArrayOf(0, 22, 63, 135, 196, 255))
+ }
+ }
+
+ @Test
+ fun testKeyboardBacklightPreferredLevels_dropsOutOfBoundsLevels() {
+ KeyboardBacklightFlags(animationEnabled = false, customLevelsEnabled = true).use {
+ val keyboardWithBacklight = createKeyboard(DEVICE_ID)
+ val suggestedLevels = intArrayOf(22, 63, 135, 400, 196, 1000)
+ val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT,
+ suggestedLevels)
+ `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
+ `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+
+ // Framework will drop out of bound levels in the config
+ assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight,
+ intArrayOf(0, 22, 63, 135, 196, 255))
+ }
+ }
+
+ private fun assertIncrementDecrementForLevels(
+ device: InputDevice,
+ light: Light,
+ expectedLevels: IntArray
+ ) {
+ val deviceId = device.id
+ val lightId = light.id
+ for (level in 1 until expectedLevels.size) {
+ incrementKeyboardBacklight(deviceId)
+ assertEquals(
+ "Light value for level $level mismatched",
+ Color.argb(expectedLevels[level], 0, 0, 0),
+ lightColorMap[lightId]
+ )
+ assertEquals(
+ "Light value for level $level must be correctly stored in the datastore",
+ expectedLevels[level],
+ dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt
+ )
+ }
+
+ // Increment above max level
+ incrementKeyboardBacklight(deviceId)
+ assertEquals(
+ "Light value for max level mismatched",
+ Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
+ lightColorMap[lightId]
+ )
+ assertEquals(
+ "Light value for max level must be correctly stored in the datastore",
+ MAX_BRIGHTNESS,
+ dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt
+ )
+
+ for (level in expectedLevels.size - 2 downTo 0) {
+ decrementKeyboardBacklight(deviceId)
+ assertEquals(
+ "Light value for level $level mismatched",
+ Color.argb(expectedLevels[level], 0, 0, 0),
+ lightColorMap[lightId]
+ )
+ assertEquals(
+ "Light value for level $level must be correctly stored in the datastore",
+ expectedLevels[level],
+ dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt
+ )
+ }
+
+ // Decrement below min level
+ decrementKeyboardBacklight(deviceId)
+ assertEquals(
+ "Light value for min level mismatched",
+ Color.argb(0, 0, 0, 0),
+ lightColorMap[lightId]
+ )
+ assertEquals(
+ "Light value for min level must be correctly stored in the datastore",
+ 0,
+ dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt
+ )
+ }
+
inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() {
override fun onBrightnessChanged(
deviceId: Int,
@@ -496,4 +618,26 @@
val maxBrightnessLevel: Int,
val isTriggeredByKeyPress: Boolean
)
+
+ private inner class KeyboardBacklightFlags constructor(
+ animationEnabled: Boolean,
+ customLevelsEnabled: Boolean
+ ) : AutoCloseable {
+ init {
+ InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(animationEnabled)
+ InputFeatureFlagProvider.setKeyboardBacklightCustomLevelsEnabled(customLevelsEnabled)
+ }
+
+ override fun close() {
+ InputFeatureFlagProvider.clearOverrides()
+ }
+ }
+
+ private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory {
+ override fun makeIntAnimator(from: Int, to: Int): ValueAnimator {
+ lastAnimationValues[0] = from
+ lastAnimationValues[1] = to
+ return ValueAnimator.ofInt(from, to)
+ }
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 676bfb0..2015ae9 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -39,6 +39,7 @@
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
import static java.util.Collections.unmodifiableMap;
@@ -59,7 +60,7 @@
class ShortcutKeyTestBase {
TestPhoneWindowManager mPhoneWindowManager;
- final Context mContext = getInstrumentation().getTargetContext();
+ final Context mContext = spy(getInstrumentation().getTargetContext());
/** Modifier key to meta state */
private static final Map<Integer, Integer> MODIFIER;
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
new file mode 100644
index 0000000..fe8017e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+/**
+ * Test class for stem key gesture.
+ *
+ * Build/Install/Run:
+ * atest WmTests:StemKeyGestureTests
+ */
+public class StemKeyGestureTests extends ShortcutKeyTestBase {
+ @Mock private Resources mResources;
+
+ /**
+ * Stem single key should not launch behavior during set up.
+ */
+ @Test
+ public void stemSingleKey_duringSetup_doNothing() {
+ stemKeySetup(
+ () -> overrideBehavior(
+ com.android.internal.R.integer.config_shortPressOnStemPrimaryBehavior,
+ SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS));
+ mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
+ mPhoneWindowManager.overrideIsUserSetupComplete(false);
+
+ sendKey(KEYCODE_STEM_PRIMARY);
+
+ mPhoneWindowManager.assertNotOpenAllAppView();
+ }
+
+ /**
+ * Stem single key should launch all app after set up.
+ */
+ @Test
+ public void stemSingleKey_AfterSetup_openAllApp() {
+ stemKeySetup(
+ () -> overrideBehavior(
+ com.android.internal.R.integer.config_shortPressOnStemPrimaryBehavior,
+ SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS));
+ mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false);
+ mPhoneWindowManager.overrideIsUserSetupComplete(true);
+
+ sendKey(KEYCODE_STEM_PRIMARY);
+
+ mPhoneWindowManager.assertOpenAllAppView();
+ }
+
+ private void stemKeySetup(Runnable behaviorOverrideRunnable) {
+ super.tearDown();
+ setupResourcesMock();
+ behaviorOverrideRunnable.run();
+ super.setUp();
+ }
+
+ private void setupResourcesMock() {
+ Resources realResources = mContext.getResources();
+
+ mResources = Mockito.mock(Resources.class);
+ doReturn(mResources).when(mContext).getResources();
+
+ doAnswer(invocation -> realResources.getXml((Integer) invocation.getArguments()[0]))
+ .when(mResources).getXml(anyInt());
+ doAnswer(invocation -> realResources.getString((Integer) invocation.getArguments()[0]))
+ .when(mResources).getString(anyInt());
+ doAnswer(invocation -> realResources.getBoolean((Integer) invocation.getArguments()[0]))
+ .when(mResources).getBoolean(anyInt());
+ }
+
+ private void overrideBehavior(int resId, int expectedBehavior) {
+ doReturn(expectedBehavior).when(mResources).getInteger(eq(resId));
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index d383024..af48cbd 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -16,6 +16,7 @@
package com.android.server.policy;
+import static android.os.Build.HW_TIMEOUT_MULTIPLIER;
import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.STATE_ON;
@@ -43,8 +44,11 @@
import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.after;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.withSettings;
@@ -64,6 +68,7 @@
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.os.Vibrator;
import android.service.dreams.DreamManagerInternal;
import android.telecom.TelecomManager;
@@ -79,6 +84,7 @@
import com.android.server.input.InputManagerInternal;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.pm.UserManagerInternal;
+import com.android.server.policy.keyguard.KeyguardServiceDelegate;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.vr.VrManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -100,6 +106,8 @@
class TestPhoneWindowManager {
private static final long SHORTCUT_KEY_DELAY_MILLIS = 150;
+ private static final long TEST_SINGLE_KEY_DELAY_MILLIS
+ = SingleKeyGestureDetector.MULTI_PRESS_TIMEOUT + 1000L * HW_TIMEOUT_MULTIPLIER;
private PhoneWindowManager mPhoneWindowManager;
private Context mContext;
@@ -134,6 +142,8 @@
@Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+ @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate;
+
private StaticMockitoSession mMockitoSession;
private HandlerThread mHandlerThread;
private Handler mHandler;
@@ -151,6 +161,10 @@
Supplier<GlobalActions> getGlobalActionsFactory() {
return () -> mGlobalActions;
}
+
+ KeyguardServiceDelegate getKeyguardServiceDelegate() {
+ return mKeyguardServiceDelegate;
+ }
}
TestPhoneWindowManager(Context context) {
@@ -158,12 +172,12 @@
mHandlerThread = new HandlerThread("fake window manager");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
- mHandler.runWithScissors(()-> setUp(context), 0 /* timeout */);
+ mContext = mockingDetails(context).isSpy() ? context : spy(context);
+ mHandler.runWithScissors(this::setUp, 0 /* timeout */);
}
- private void setUp(Context context) {
+ private void setUp() {
mPhoneWindowManager = spy(new PhoneWindowManager());
- mContext = spy(context);
// Use stubOnly() to reduce memory usage if it doesn't need verification.
final MockSettings spyStubOnly = withSettings().stubOnly()
@@ -251,6 +265,7 @@
overrideLaunchAccessibility();
doReturn(false).when(mPhoneWindowManager).keyguardOn();
doNothing().when(mContext).startActivityAsUser(any(), any());
+ doNothing().when(mContext).startActivityAsUser(any(), any(), any());
Mockito.reset(mContext);
}
@@ -381,6 +396,14 @@
doNothing().when(mPhoneWindowManager).launchHomeFromHotKey(anyInt());
}
+ void overrideIsUserSetupComplete(boolean isCompleted) {
+ doReturn(isCompleted).when(mPhoneWindowManager).isUserSetupComplete();
+ }
+
+ void setKeyguardServiceDelegateIsShowing(boolean isShowing) {
+ doReturn(isShowing).when(mKeyguardServiceDelegate).isShowing();
+ }
+
/**
* Below functions will check the policy behavior could be invoked.
*/
@@ -514,4 +537,18 @@
waitForIdle();
verify(mPhoneWindowManager).launchHomeFromHotKey(anyInt());
}
+
+ void assertOpenAllAppView() {
+ waitForIdle();
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mContext, timeout(TEST_SINGLE_KEY_DELAY_MILLIS))
+ .startActivityAsUser(intentCaptor.capture(), isNull(), any(UserHandle.class));
+ Assert.assertEquals(Intent.ACTION_ALL_APPS, intentCaptor.getValue().getAction());
+ }
+
+ void assertNotOpenAllAppView() {
+ waitForIdle();
+ verify(mContext, after(TEST_SINGLE_KEY_DELAY_MILLIS).never())
+ .startActivityAsUser(any(Intent.class), any(), any(UserHandle.class));
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index cb984f8..77e944f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3329,7 +3329,7 @@
// app1 requests IME visible.
app1.setRequestedVisibleTypes(ime(), ime());
- mDisplayContent.getInsetsStateController().onInsetsModified(app1);
+ mDisplayContent.getInsetsStateController().onRequestedVisibleTypesChanged(app1);
// Verify app1's IME insets is visible and app2's IME insets frozen flag set.
assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
@@ -3398,7 +3398,7 @@
assertFalse(activity2.mImeInsetsFrozenUntilStartInput);
app1.setRequestedVisibleTypes(ime());
- controller.onInsetsModified(app1);
+ controller.onRequestedVisibleTypesChanged(app1);
// Expect all activities in split-screen will get IME insets visible state
assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 2b589bf..d179338 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -1702,6 +1702,9 @@
@Test
public void testRecordActivityMovementBeforeDeliverToTop() {
+ // Mock recents as task is only marked to be in recents
+ mAtm.mTaskSupervisor.setRecentTasks(mock(RecentTasks.class));
+
final Task task = new TaskBuilder(mAtm.mTaskSupervisor).build();
final ActivityRecord activityBot = new ActivityBuilder(mAtm).setTask(task).build();
final ActivityRecord activityTop = new ActivityBuilder(mAtm).setTask(task).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index b0609dd..dd90e04 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -182,6 +182,44 @@
dimmingNonImTarget, imeNonDrawNavBar, NAV_BAR_BOTTOM));
}
+ @Test
+ public void testChooseNavigationBackgroundWindow() {
+ final WindowState drawBarWin = createOpaqueFullscreen(false);
+ final WindowState nonDrawBarWin = createDimmingDialogWindow(true);
+
+ final WindowState visibleIme = createInputMethodWindow(true, true, false);
+ final WindowState invisibleIme = createInputMethodWindow(false, true, false);
+ final WindowState nonDrawBarIme = createInputMethodWindow(true, false, false);
+
+ assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow(
+ drawBarWin, null, NAV_BAR_BOTTOM));
+ assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+ null, null, NAV_BAR_BOTTOM));
+ assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+ nonDrawBarWin, null, NAV_BAR_BOTTOM));
+
+ assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow(
+ drawBarWin, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow(
+ null, visibleIme, NAV_BAR_BOTTOM));
+ assertEquals(visibleIme, DisplayPolicy.chooseNavigationBackgroundWindow(
+ nonDrawBarWin, visibleIme, NAV_BAR_BOTTOM));
+
+ assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow(
+ drawBarWin, invisibleIme, NAV_BAR_BOTTOM));
+ assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+ null, invisibleIme, NAV_BAR_BOTTOM));
+ assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+ nonDrawBarWin, invisibleIme, NAV_BAR_BOTTOM));
+
+ assertEquals(drawBarWin, DisplayPolicy.chooseNavigationBackgroundWindow(
+ drawBarWin, nonDrawBarIme, NAV_BAR_BOTTOM));
+ assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+ null, nonDrawBarIme, NAV_BAR_BOTTOM));
+ assertNull(DisplayPolicy.chooseNavigationBackgroundWindow(
+ nonDrawBarWin, nonDrawBarIme, NAV_BAR_BOTTOM));
+ }
+
@SetupWindows(addWindows = W_NAVIGATION_BAR)
@Test
public void testUpdateLightNavigationBarLw() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index a8fc25f..204cbf7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -370,7 +370,7 @@
mAppWindow.setRequestedVisibleTypes(
navigationBars() | statusBars(), navigationBars() | statusBars());
- policy.onInsetsModified(mAppWindow);
+ policy.onRequestedVisibleTypesChanged(mAppWindow);
waitUntilWindowAnimatorIdle();
controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index d7e736d..114796d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -192,17 +192,16 @@
// This can be the IME z-order target while app cannot be the IME z-order target.
// This is also the only IME control target in this test, so IME won't be invisible caused
// by the control-target change.
- mDisplayContent.updateImeInputAndControlTarget(
- createWindow(null, TYPE_APPLICATION, "base"));
+ final WindowState base = createWindow(null, TYPE_APPLICATION, "base");
+ mDisplayContent.updateImeInputAndControlTarget(base);
// Make IME and stay visible during the test.
mImeWindow.setHasSurface(true);
getController().getOrCreateSourceProvider(ID_IME, ime())
.setWindowContainer(mImeWindow, null, null);
- getController().onImeControlTargetChanged(
- mDisplayContent.getImeInputTarget().getWindowState());
- mDisplayContent.getImeInputTarget().getWindowState().setRequestedVisibleTypes(ime(), ime());
- getController().onInsetsModified(mDisplayContent.getImeInputTarget().getWindowState());
+ getController().onImeControlTargetChanged(base);
+ base.setRequestedVisibleTypes(ime(), ime());
+ getController().onRequestedVisibleTypesChanged(base);
// Send our spy window (app) into the system so that we can detect the invocation.
final WindowState win = createWindow(null, TYPE_APPLICATION, "app");
@@ -485,7 +484,7 @@
mDisplayContent.updateImeInputAndControlTarget(app);
app.setRequestedVisibleTypes(ime(), ime());
- getController().onInsetsModified(app);
+ getController().onRequestedVisibleTypesChanged(app);
assertTrue(ime.getControllableInsetProvider().getSource().isVisible());
getController().updateAboveInsetsState(true /* notifyInsetsChange */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 7d507e9..34a13bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -38,6 +38,7 @@
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH;
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
@@ -461,7 +462,7 @@
public void testGetCropBoundsIfNeeded_handleCropForTransparentActivityBasedOnOpaqueBounds() {
final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
WindowInsets.Type.navigationBars());
- taskbar.setInsetsRoundedCornerFrame(true);
+ taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
final Rect opaqueBounds = new Rect(0, 0, 500, 300);
doReturn(opaqueBounds).when(mActivity).getBounds();
@@ -505,7 +506,7 @@
public void testGetCropBoundsIfNeeded_appliesCrop() {
final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
WindowInsets.Type.navigationBars());
- taskbar.setInsetsRoundedCornerFrame(true);
+ taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
// Apply crop if taskbar is expanded
@@ -528,7 +529,7 @@
public void testGetCropBoundsIfNeeded_appliesCropWithSizeCompatScaling() {
final InsetsSource taskbar = new InsetsSource(/*id=*/ 0,
WindowInsets.Type.navigationBars());
- taskbar.setInsetsRoundedCornerFrame(true);
+ taskbar.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
final WindowState mainWindow = mockForGetCropBoundsAndRoundedCorners(taskbar);
final float scaling = 2.0f;
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 0ccb0d0..b02b774 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -49,10 +49,12 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static java.lang.Integer.MAX_VALUE;
@@ -1139,6 +1141,40 @@
}
@Test
+ public void addTask_taskAlreadyInRecentsMovedToTop_callsTaskNotificationController() {
+ final Task firstTask = createTaskBuilder(".Task").build();
+ final Task secondTask = createTaskBuilder(".Task2").build();
+
+ mRecentTasks.add(firstTask);
+ mRecentTasks.add(secondTask);
+
+ TaskChangeNotificationController controller =
+ mAtm.getTaskChangeNotificationController();
+ clearInvocations(controller);
+
+ // Add firstTask back to top
+ mRecentTasks.add(firstTask);
+ verify(controller).notifyTaskListUpdated();
+ }
+
+ @Test
+ public void addTask_taskAlreadyInRecentsOnTop_doesNotNotify() {
+ final Task firstTask = createTaskBuilder(".Task").build();
+ final Task secondTask = createTaskBuilder(".Task2").build();
+
+ mRecentTasks.add(firstTask);
+ mRecentTasks.add(secondTask);
+
+ TaskChangeNotificationController controller =
+ mAtm.getTaskChangeNotificationController();
+ clearInvocations(controller);
+
+ // Add secondTask to top again
+ mRecentTasks.add(secondTask);
+ verifyZeroInteractions(controller);
+ }
+
+ @Test
public void removeTask_callsTaskNotificationController() {
final Task task = createTaskBuilder(".Task").build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 2dd34eb..d91be16 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -30,6 +30,7 @@
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.provider.DeviceConfig.NAMESPACE_CONSTRAIN_DISPLAY_APIS;
+import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
@@ -3483,7 +3484,7 @@
final InsetsSource navSource = new InsetsSource(
InsetsSource.createId(null, 0, navigationBars()), navigationBars());
- navSource.setInsetsRoundedCornerFrame(true);
+ navSource.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
mActivity.mWmService.mLetterboxConfiguration.setLetterboxActivityCornersRadius(15);
@@ -3531,7 +3532,7 @@
final InsetsSource navSource = new InsetsSource(
InsetsSource.createId(null, 0, navigationBars()), navigationBars());
- navSource.setInsetsRoundedCornerFrame(true);
+ navSource.setFlags(FLAG_INSETS_ROUNDED_CORNER, FLAG_INSETS_ROUNDED_CORNER);
// Immersive activity has transient navbar
navSource.setVisible(!immersive);
navSource.setFrame(new Rect(0, screenHeight - taskbarHeight, screenWidth, screenHeight));
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index 4ba538e..4c55f7f 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -23,14 +23,49 @@
default_applicable_licenses: ["frameworks_base_license"],
}
-android_test {
- name: "FlickerTests",
+filegroup {
+ name: "FlickerTestsBase-src",
+ srcs: ["src/com/android/server/wm/flicker/*.kt"],
+}
+
+filegroup {
+ name: "FlickerTestsAppClose-src",
+ srcs: ["src/**/close/*.kt"],
+}
+
+filegroup {
+ name: "FlickerTestsActivityEmbedding-src",
srcs: [
- "src/**/*.java",
- "src/**/*.kt",
+ "src/**/activityembedding/*.kt",
+ "src/**/activityembedding/close/*.kt",
+ "src/**/activityembedding/rotation/*.kt",
],
- manifest: "AndroidManifest.xml",
- test_config: "AndroidTest.xml",
+}
+
+filegroup {
+ name: "FlickerTestsIme-src",
+ srcs: ["src/**/ime/*.kt"],
+}
+
+filegroup {
+ name: "FlickerTestsAppLaunch-src",
+ srcs: ["src/**/launch/*.kt"],
+}
+
+filegroup {
+ name: "FlickerTestsQuickswitch-src",
+ srcs: ["src/**/quickswitch/*.kt"],
+}
+
+filegroup {
+ name: "FlickerTestsRotation-src",
+ srcs: ["src/**/rotation/*.kt"],
+}
+
+java_defaults {
+ name: "FlickerTestsDefault",
+ manifest: "manifests/AndroidManifest.xml",
+ test_config_template: "AndroidTestTemplate.xml",
platform_apis: true,
certificate: "platform",
optimize: {
@@ -42,16 +77,98 @@
"androidx.test.ext.junit",
"flickertestapplib",
"flickerlib",
- "flickerlib-apphelpers",
"flickerlib-helpers",
- "truth-prebuilt",
- "launcher-helper-lib",
- "launcher-aosp-tapl",
"platform-test-annotations",
- "wm-flicker-window-extensions",
+ "wm-flicker-common-app-helpers",
],
data: [
":FlickerTestApp",
+ "trace_config/*",
+ ],
+}
+
+android_test {
+ name: "FlickerTestsOther",
+ defaults: ["FlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestOther.xml"],
+ package_name: "com.android.server.wm.flicker",
+ instrumentation_target_package: "com.android.server.wm.flicker",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ exclude_srcs: [
+ ":FlickerTestsAppClose-src",
+ ":FlickerTestsIme-src",
+ ":FlickerTestsAppLaunch-src",
+ ":FlickerTestsQuickswitch-src",
+ ":FlickerTestsRotation-src",
+ ],
+}
+
+android_test {
+ name: "FlickerTestsAppClose",
+ defaults: ["FlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestAppClose.xml"],
+ package_name: "com.android.server.wm.flicker.close",
+ instrumentation_target_package: "com.android.server.wm.flicker.close",
+ srcs: [
+ ":FlickerTestsBase-src",
+ ":FlickerTestsAppClose-src",
+ ],
+ exclude_srcs: [
+ ":FlickerTestsActivityEmbedding-src",
+ ],
+}
+
+android_test {
+ name: "FlickerTestsIme",
+ defaults: ["FlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestIme.xml"],
+ package_name: "com.android.server.wm.flicker.ime",
+ instrumentation_target_package: "com.android.server.wm.flicker.ime",
+ srcs: [
+ ":FlickerTestsBase-src",
+ ":FlickerTestsIme-src",
+ ],
+}
+
+android_test {
+ name: "FlickerTestsAppLaunch",
+ defaults: ["FlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestAppLaunch.xml"],
+ package_name: "com.android.server.wm.flicker.launch",
+ instrumentation_target_package: "com.android.server.wm.flicker.launch",
+ srcs: [
+ ":FlickerTestsBase-src",
+ ":FlickerTestsAppLaunch-src",
+ ],
+}
+
+android_test {
+ name: "FlickerTestsQuickswitch",
+ defaults: ["FlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestQuickswitch.xml"],
+ package_name: "com.android.server.wm.flicker.rotation",
+ instrumentation_target_package: "com.android.server.wm.flicker.rotation",
+ srcs: [
+ ":FlickerTestsBase-src",
+ ":FlickerTestsQuickswitch-src",
+ ],
+}
+
+android_test {
+ name: "FlickerTestsRotation",
+ defaults: ["FlickerTestsDefault"],
+ additional_manifests: ["manifests/AndroidManifestRotation.xml"],
+ package_name: "com.android.server.wm.flicker.rotation",
+ instrumentation_target_package: "com.android.server.wm.flicker.rotation",
+ srcs: [
+ ":FlickerTestsBase-src",
+ ":FlickerTestsRotation-src",
+ ],
+ exclude_srcs: [
+ ":FlickerTestsActivityEmbedding-src",
],
}
diff --git a/tests/FlickerTests/AndroidTest.xml b/tests/FlickerTests/AndroidTest.xml
deleted file mode 100644
index 32ff243..0000000
--- a/tests/FlickerTests/AndroidTest.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright 2018 Google Inc. All Rights Reserved.
- -->
-<configuration description="Runs WindowManager Flicker Tests">
- <option name="test-tag" value="FlickerTests" />
- <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
- <!-- keeps the screen on during tests -->
- <option name="screen-always-on" value="on" />
- <!-- prevents the phone from restarting -->
- <option name="force-skip-system-props" value="true" />
- <!-- set WM tracing verbose level to all -->
- <option name="run-command" value="cmd window tracing level all" />
- <!-- set WM tracing to frame (avoid incomplete states) -->
- <option name="run-command" value="cmd window tracing frame" />
- <!-- ensure lock screen mode is swipe -->
- <option name="run-command" value="locksettings set-disabled false" />
- <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
- <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
- <!-- restart launcher to activate TAPL -->
- <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
- <!-- Ensure output directory is empty at the start -->
- <option name="run-command" value="rm -rf /sdcard/flicker" />
- <!-- Increase trace size: 20mb for WM and 80mb for SF -->
- <option name="run-command" value="cmd window tracing size 20480" />
- <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920" />
- </target_preparer>
- <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
- <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" />
- <option name="run-command" value="settings put system show_touches 1" />
- <option name="run-command" value="settings put system pointer_location 1" />
- <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard" />
- <option name="teardown-command" value="settings delete system show_touches" />
- <option name="teardown-command" value="settings delete system pointer_location" />
- <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural" />
- </target_preparer>
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="cleanup-apks" value="true"/>
- <option name="test-file-name" value="FlickerTests.apk"/>
- <option name="test-file-name" value="FlickerTestApp.apk" />
- </target_preparer>
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="package" value="com.android.server.wm.flicker"/>
- <option name="shell-timeout" value="6600s" />
- <option name="test-timeout" value="6600s" />
- <option name="hidden-api-checks" value="false" />
- </test>
- <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
- <option name="directory-keys" value="/sdcard/flicker" />
- <option name="collect-on-run-ended-only" value="true" />
- <option name="clean-up" value="true" />
- </metrics_collector>
-</configuration>
diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/AndroidTestTemplate.xml
new file mode 100644
index 0000000..ec01317
--- /dev/null
+++ b/tests/FlickerTests/AndroidTestTemplate.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ -->
+<configuration description="Runs WindowManager {MODULE}">
+ <option name="test-tag" value="FlickerTests" />
+ <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
+ <option name="isolated-storage" value="false" />
+
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- keeps the screen on during tests -->
+ <option name="screen-always-on" value="on" />
+ <!-- prevents the phone from restarting -->
+ <option name="force-skip-system-props" value="true" />
+ <!-- set WM tracing verbose level to all -->
+ <option name="run-command" value="cmd window tracing level all" />
+ <!-- set WM tracing to frame (avoid incomplete states) -->
+ <option name="run-command" value="cmd window tracing frame" />
+ <!-- ensure lock screen mode is swipe -->
+ <option name="run-command" value="locksettings set-disabled false" />
+ <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests -->
+ <option name="run-command" value="pm disable com.google.android.internal.betterbug" />
+ <!-- restart launcher to activate TAPL -->
+ <option name="run-command" value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher" />
+ <!-- Ensure output directory is empty at the start -->
+ <option name="run-command" value="rm -rf /sdcard/flicker" />
+ <!-- Increase trace size: 20mb for WM and 80mb for SF -->
+ <option name="run-command" value="cmd window tracing size 20480" />
+ <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1" />
+ <option name="run-command" value="settings put system show_touches 1" />
+ <option name="run-command" value="settings put system pointer_location 1" />
+ <option name="teardown-command"
+ value="settings delete secure show_ime_with_hard_keyboard" />
+ <option name="teardown-command" value="settings delete system show_touches" />
+ <option name="teardown-command" value="settings delete system pointer_location" />
+ <option name="teardown-command"
+ value="cmd overlay enable com.android.internal.systemui.navbar.gestural" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true"/>
+ <option name="test-file-name" value="{MODULE}.apk"/>
+ <option name="test-file-name" value="FlickerTestApp.apk" />
+ </target_preparer>
+ <!-- Needed for pushing the trace config file -->
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+ <option name="push-file"
+ key="trace_config.textproto"
+ value="/data/misc/perfetto-traces/trace_config.textproto"
+ />
+ <!--Install the content provider automatically when we push some file in sdcard folder.-->
+ <!--Needed to avoid the installation during the test suite.-->
+ <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="{PACKAGE}"/>
+ <option name="shell-timeout" value="6600s" />
+ <option name="test-timeout" value="6600s" />
+ <option name="hidden-api-checks" value="false" />
+ <option name="device-listeners" value="android.device.collectors.PerfettoListener" />
+ <!-- PerfettoListener related arguments -->
+ <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+ <option name="instrumentation-arg"
+ key="perfetto_config_file"
+ value="trace_config.textproto"
+ />
+ <option name="instrumentation-arg" key="per_run" value="true" />
+ </test>
+ <!-- Needed for pulling the collected trace config on to the host -->
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="pull-pattern-keys" value="perfetto_file_path" />
+ </metrics_collector>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="pull-pattern-keys" value="(\w)+\.winscope" />
+ <option name="pull-pattern-keys" value="(\w)+\.mp4" />
+ <option name="collect-on-run-ended-only" value="false" />
+ <option name="clean-up" value="true" />
+ </metrics_collector>
+</configuration>
diff --git a/tests/FlickerTests/AndroidManifest.xml b/tests/FlickerTests/manifests/AndroidManifest.xml
similarity index 70%
rename from tests/FlickerTests/AndroidManifest.xml
rename to tests/FlickerTests/manifests/AndroidManifest.xml
index 462f91b..de8a3c6 100644
--- a/tests/FlickerTests/AndroidManifest.xml
+++ b/tests/FlickerTests/manifests/AndroidManifest.xml
@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 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.
--->
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.wm.flicker">
@@ -47,9 +48,4 @@
<uses-library android:name="android.test.runner"/>
<uses-library android:name="androidx.window.extensions" android:required="false"/>
</application>
-
- <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.server.wm.flicker"
- android:label="WindowManager Flicker Tests">
- </instrumentation>
</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestAppClose.xml b/tests/FlickerTests/manifests/AndroidManifestAppClose.xml
new file mode 100644
index 0000000..4cdcb90
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestAppClose.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.wm.flicker.close">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.wm.flicker.close"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml b/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml
new file mode 100644
index 0000000..659a745
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestAppLaunch.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.wm.flicker.launch">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.wm.flicker.launch"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestIme.xml b/tests/FlickerTests/manifests/AndroidManifestIme.xml
new file mode 100644
index 0000000..abd03af
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestIme.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.wm.flicker.ime">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.wm.flicker.ime"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestOther.xml b/tests/FlickerTests/manifests/AndroidManifestOther.xml
new file mode 100644
index 0000000..47749b8
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestOther.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.wm.flicker">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.wm.flicker"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml b/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml
new file mode 100644
index 0000000..203035d
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestQuickswitch.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.wm.flicker.quickswitch">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.wm.flicker.quickswitch"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/manifests/AndroidManifestRotation.xml b/tests/FlickerTests/manifests/AndroidManifestRotation.xml
new file mode 100644
index 0000000..2852cf2
--- /dev/null
+++ b/tests/FlickerTests/manifests/AndroidManifestRotation.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.wm.flicker.rotation">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.wm.flicker.rotation"
+ android:label="WindowManager Flicker Tests">
+ </instrumentation>
+</manifest>
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt
new file mode 100644
index 0000000..c0c738b
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test closing a secondary activity in a split.
+ *
+ * Setup: Launch A|B in split with B being the secondary activity.
+ * Transitions: Finish B and expect A to become fullscreen.
+ *
+ * To run this test: `atest FlickerTests:CloseSecondaryActivityInSplitTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class CloseSecondaryActivityInSplitTest(flicker: FlickerTest) :
+ ActivityEmbeddingTestBase(flicker) {
+
+ override val transition: FlickerBuilder.() -> Unit = {
+ setup {
+ tapl.setExpectedRotationCheckEnabled(false)
+ // Launches fullscreen A.
+ testApp.launchViaIntent(wmHelper)
+ // Launches a split A|B and waits for both activities to show.
+ testApp.launchSecondaryActivity(wmHelper)
+ // Get fullscreen bounds
+ startDisplayBounds =
+ wmHelper.currentState.layerState.physicalDisplayBounds ?:
+ error("Can't get display bounds")
+ }
+ transitions {
+ // Finish secondary activity B.
+ testApp.finishSecondaryActivity(wmHelper)
+ // Expect the main activity A to expand into fullscreen.
+ wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
+ }
+ teardown {
+ tapl.goHome()
+ testApp.exit(wmHelper)
+ }
+ }
+
+ /** Main activity is always visible and becomes fullscreen in the end. */
+ @Presubmit
+ @Test
+ fun mainActivityWindowBecomesFullScreen() {
+ flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+ flicker.assertWmEnd {
+ this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .coversExactly(startDisplayBounds)
+ }
+ }
+
+ /** Main activity surface is animated from split to fullscreen. */
+ @Presubmit
+ @Test
+ fun mainActivityLayerIsAlwaysVisible() {
+ flicker.assertLayers {
+ isVisible(
+ ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT.or(
+ ComponentNameMatcher.TRANSITION_SNAPSHOT
+ )
+ )
+ }
+ flicker.assertLayersEnd {
+ isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .isInvisible(ComponentNameMatcher.TRANSITION_SNAPSHOT)
+ }
+ }
+
+ /** Secondary activity should destroy and become invisible. */
+ @Presubmit
+ @Test
+ fun secondaryActivityWindowFinishes() {
+ flicker.assertWm {
+ contains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ .then()
+ .notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun secondaryActivityLayerFinishes() {
+ flicker.assertLayers {
+ isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ .then()
+ .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ }
+ }
+
+ companion object {
+ /** {@inheritDoc} */
+ private var startDisplayBounds = Rect.EMPTY
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.nonRotationTests()
+ }
+ }
+ }
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
new file mode 100644
index 0000000..39ae8e2
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
+import com.android.server.wm.flicker.rotation.RotationTransition
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.FlickerTest
+import android.tools.device.flicker.legacy.FlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Tests rotating two activities in an Activity Embedding split.
+ *
+ * Setup: Launch A|B in split with B being the secondary activity.
+ * Transitions: Rotate display, and expect A and B to split evenly in new rotation.
+ *
+ * To run this test: `atest FlickerTests:RotateSplitNoChangeTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class RotateSplitNoChangeTest(flicker: FlickerTest) : RotationTransition(flicker) {
+
+ override val testApp = ActivityEmbeddingAppHelper(instrumentation)
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ super.transition(this)
+ setup {
+ testApp.launchViaIntent(wmHelper)
+ testApp.launchSecondaryActivity(wmHelper)
+ }
+ }
+
+ /**
+ * Checks that the [ComponentNameMatcher.ROTATION] layer appears during the transition, doesn't
+ * flicker, and disappears before the transition is complete
+ */
+ @Presubmit
+ @Test
+ fun rotationLayerAppearsAndVanishes() {
+ flicker.assertLayers {
+ this.isVisible(testApp)
+ .then()
+ .isVisible(ComponentNameMatcher.ROTATION)
+ .then()
+ .isVisible(testApp)
+ .isInvisible(ComponentNameMatcher.ROTATION)
+ }
+ }
+
+ /**
+ * Overrides inherited assertion because in AE Split, the main and secondary activity are separate
+ * layers, each covering up exactly half of the display.
+ */
+ @Presubmit
+ @Test
+ override fun appLayerRotates_StartingPos() {
+ flicker.assertLayersStart {
+ this.entry.displays.map { display ->
+ val leftLayerRegion = this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ val rightLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ // Compare dimensions of two splits, given we're using default split attributes,
+ // both activities take up the same visible size on the display.
+ check{"height"}.that(leftLayerRegion.region.height).isEqual(rightLayerRegion.region.height)
+ check{"width"}.that(leftLayerRegion.region.width).isEqual(rightLayerRegion.region.width)
+ leftLayerRegion.notOverlaps(rightLayerRegion.region)
+ // Layers of two activities sum to be fullscreen size on display.
+ leftLayerRegion.plus(rightLayerRegion.region).coversExactly(display.layerStackSpace)
+ }
+ }
+ }
+
+ /**
+ * Verifies dimensions of both split activities hold their invariance after transition too.
+ */
+ @Presubmit
+ @Test
+ override fun appLayerRotates_EndingPos() {
+ flicker.assertLayersEnd {
+ this.entry.displays.map { display ->
+ val leftLayerRegion = this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ val rightLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ check{"height"}.that(leftLayerRegion.region.height).isEqual(rightLayerRegion.region.height)
+ check{"width"}.that(leftLayerRegion.region.width).isEqual(rightLayerRegion.region.width)
+ leftLayerRegion.notOverlaps(rightLayerRegion.region)
+ leftLayerRegion.plus(rightLayerRegion.region).coversExactly(display.layerStackSpace)
+ }
+ }
+ }
+
+ /** Both activities in split should remain visible during rotation. */
+ @Presubmit
+ @Test
+ fun bothActivitiesAreAlwaysVisible() {
+ flicker.assertWm {
+ isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ }
+ flicker.assertWm {
+ isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return FlickerTestFactory.rotationTests()
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index daecfe7..e019b2b 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -60,6 +60,24 @@
}
/**
+ * Clicks the button to finishes the secondary activity launched through
+ * [launchSecondaryActivity], waits for the main activity to resume.
+ */
+ fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) {
+ val finishButton =
+ uiDevice.wait(
+ Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
+ FIND_TIMEOUT
+ )
+ require(finishButton != null) { "Can't find finish secondary activity button on screen." }
+ finishButton.click()
+ wmHelper
+ .StateSyncBuilder()
+ .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
+ .waitForAndVerify()
+ }
+
+ /**
* Clicks the button to launch the placeholder primary activity, which should launch the
* placeholder secondary activity based on the placeholder rule.
*/
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml
index 3a02cad..f0dfdfc 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_base_layout.xml
@@ -20,5 +20,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
-
</LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
new file mode 100644
index 0000000..239aba5
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/secondary_activity_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/finish_secondary_activity_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:text="Finish" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index 00f4c25..6e78750 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -16,15 +16,28 @@
package com.android.server.wm.flicker.testapp;
+import android.app.Activity;
import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
/**
* Activity to be used as the secondary activity to split with
* {@link ActivityEmbeddingMainActivity}.
*/
-public class ActivityEmbeddingSecondaryActivity extends ActivityEmbeddingBaseActivity {
+public class ActivityEmbeddingSecondaryActivity extends Activity {
+
@Override
- int getBackgroundColor() {
- return Color.YELLOW;
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_embedding_secondary_activity_layout);
+ findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW);
+ findViewById(R.id.finish_secondary_activity_button).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
}
}
diff --git a/tests/FlickerTests/trace_config/trace_config.textproto b/tests/FlickerTests/trace_config/trace_config.textproto
new file mode 100644
index 0000000..c9a35ac
--- /dev/null
+++ b/tests/FlickerTests/trace_config/trace_config.textproto
@@ -0,0 +1,77 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# proto-message: TraceConfig
+
+# Enable periodic flushing of the trace buffer into the output file.
+write_into_file: true
+
+# Writes the userspace buffer into the file every 1s.
+file_write_period_ms: 2500
+
+# See b/126487238 - we need to guarantee ordering of events.
+flush_period_ms: 30000
+
+# The trace buffers needs to be big enough to hold |file_write_period_ms| of
+# trace data. The trace buffer sizing depends on the number of trace categories
+# enabled and the device activity.
+
+# RSS events
+buffers: {
+ size_kb: 63488
+ fill_policy: RING_BUFFER
+}
+
+data_sources {
+ config {
+ name: "linux.process_stats"
+ target_buffer: 0
+ # polled per-process memory counters and process/thread names.
+ # If you don't want the polled counters, remove the "process_stats_config"
+ # section, but keep the data source itself as it still provides on-demand
+ # thread/process naming for ftrace data below.
+ process_stats_config {
+ scan_all_processes_on_start: true
+ }
+ }
+}
+
+data_sources: {
+ config {
+ name: "linux.ftrace"
+ ftrace_config {
+ ftrace_events: "ftrace/print"
+ ftrace_events: "task/task_newtask"
+ ftrace_events: "task/task_rename"
+ atrace_categories: "ss"
+ atrace_categories: "wm"
+ atrace_categories: "am"
+ atrace_categories: "aidl"
+ atrace_categories: "input"
+ atrace_categories: "binder_driver"
+ atrace_categories: "sched_process_exit"
+ atrace_apps: "com.android.server.wm.flicker"
+ atrace_apps: "com.android.server.wm.flicker.other"
+ atrace_apps: "com.android.server.wm.flicker.close"
+ atrace_apps: "com.android.server.wm.flicker.ime"
+ atrace_apps: "com.android.server.wm.flicker.launch"
+ atrace_apps: "com.android.server.wm.flicker.quickswitch"
+ atrace_apps: "com.android.server.wm.flicker.rotation"
+ atrace_apps: "com.android.server.wm.flicker.testapp"
+ atrace_apps: "com.android.systemui"
+ atrace_apps: "com.google.android.apps.nexuslauncher"
+ }
+ }
+}
+