Merge "AudioService: Fix ring over SCO sequence." into main
diff --git a/core/api/current.txt b/core/api/current.txt
index ff068f1..94481b6 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -55041,6 +55041,7 @@
field public static final int CONTENT_CHANGE_TYPE_DRAG_STARTED = 128; // 0x80
field public static final int CONTENT_CHANGE_TYPE_ENABLED = 4096; // 0x1000
field public static final int CONTENT_CHANGE_TYPE_ERROR = 2048; // 0x800
+ field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int CONTENT_CHANGE_TYPE_EXPANDED = 16384; // 0x4000
field public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 16; // 0x10
field public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 32; // 0x20
field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8
@@ -55190,6 +55191,7 @@
method public CharSequence getContentDescription();
method public int getDrawingOrder();
method public CharSequence getError();
+ method @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public int getExpandedState();
method @Nullable public android.view.accessibility.AccessibilityNodeInfo.ExtraRenderingInfo getExtraRenderingInfo();
method public android.os.Bundle getExtras();
method public CharSequence getHintText();
@@ -55282,6 +55284,7 @@
method public void setEditable(boolean);
method public void setEnabled(boolean);
method public void setError(CharSequence);
+ method @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public void setExpandedState(int);
method public void setFocusable(boolean);
method public void setFocused(boolean);
method @FlaggedApi("android.view.accessibility.granular_scrolling") public void setGranularScrollingSupported(boolean);
@@ -55368,6 +55371,10 @@
field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_PARTIAL = 2; // 0x2
field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_TRUE = 1; // 0x1
field @NonNull public static final android.os.Parcelable.Creator<android.view.accessibility.AccessibilityNodeInfo> CREATOR;
+ field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int EXPANDED_STATE_COLLAPSED = 1; // 0x1
+ field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int EXPANDED_STATE_FULL = 3; // 0x3
+ field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int EXPANDED_STATE_PARTIAL = 2; // 0x2
+ field @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public static final int EXPANDED_STATE_UNDEFINED = 0; // 0x0
field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
field public static final int EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_MAX_LENGTH = 20000; // 0x4e20
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index f2a36e9..768b70c 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -39,6 +39,7 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ShortcutInfo;
import android.graphics.drawable.Icon;
@@ -1344,11 +1345,15 @@
*/
@FlaggedApi(Flags.FLAG_MODES_API)
public boolean areAutomaticZenRulesUserManaged() {
- // modes ui is dependent on modes api
- return Flags.modesApi() && Flags.modesUi();
+ if (Flags.modesApi() && Flags.modesUi()) {
+ PackageManager pm = mContext.getPackageManager();
+ return !pm.hasSystemFeature(PackageManager.FEATURE_WATCH)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ } else {
+ return false;
+ }
}
-
/**
* Returns AutomaticZenRules owned by the caller.
*
diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl
index ecea479..4fdbf1e9e 100644
--- a/core/java/android/content/pm/IPackageInstaller.aidl
+++ b/core/java/android/content/pm/IPackageInstaller.aidl
@@ -95,8 +95,8 @@
void reportUnarchivalStatus(int unarchiveId, int status, long requiredStorageBytes, in PendingIntent userActionIntent, in UserHandle userHandle);
@EnforcePermission("VERIFICATION_AGENT")
- int getVerificationPolicy();
+ int getVerificationPolicy(int userId);
@EnforcePermission("VERIFICATION_AGENT")
- boolean setVerificationPolicy(int policy);
+ boolean setVerificationPolicy(int policy, int userId);
}
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 5da1444..54c5596 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -1613,7 +1613,7 @@
@RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
public final @VerificationPolicy int getVerificationPolicy() {
try {
- return mInstaller.getVerificationPolicy();
+ return mInstaller.getVerificationPolicy(mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1631,7 +1631,7 @@
@RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
public final boolean setVerificationPolicy(@VerificationPolicy int policy) {
try {
- return mInstaller.setVerificationPolicy(policy);
+ return mInstaller.setVerificationPolicy(policy, mUserId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 6669754..1206855 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -141,3 +141,10 @@
description: "Adds shortcuts to toggle and control a11y features"
bug: "373458181"
}
+
+flag {
+ name: "override_power_key_behavior_in_focused_window"
+ namespace: "input_native"
+ description: "Allows privileged focused windows to capture power key events."
+ bug: "357144512"
+}
diff --git a/core/java/android/service/notification/SystemZenRules.java b/core/java/android/service/notification/SystemZenRules.java
index 1d18643..ebb8569 100644
--- a/core/java/android/service/notification/SystemZenRules.java
+++ b/core/java/android/service/notification/SystemZenRules.java
@@ -19,6 +19,7 @@
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.StringRes;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
@@ -122,7 +123,7 @@
public static String getTriggerDescriptionForScheduleTime(Context context,
@NonNull ScheduleInfo schedule) {
final StringBuilder sb = new StringBuilder();
- String daysSummary = getShortDaysSummary(context, schedule);
+ String daysSummary = getDaysOfWeekShort(context, schedule);
if (daysSummary == null) {
// no use outputting times without dates
return null;
@@ -135,11 +136,35 @@
}
/**
- * Returns an ordered summarized list of the days on which this schedule applies, with
- * adjacent days grouped together ("Sun-Wed" instead of "Sun,Mon,Tue,Wed").
+ * Returns a short, ordered summarized list of the days on which this schedule applies, using
+ * abbreviated week days, with adjacent days grouped together ("Sun-Wed" instead of
+ * "Sun,Mon,Tue,Wed").
*/
@Nullable
- public static String getShortDaysSummary(Context context, @NonNull ScheduleInfo schedule) {
+ public static String getDaysOfWeekShort(Context context, @NonNull ScheduleInfo schedule) {
+ return getDaysSummary(context, R.string.zen_mode_trigger_summary_range_symbol_combination,
+ new SimpleDateFormat("EEE", getLocale(context)), schedule);
+ }
+
+ /**
+ * Returns a string representing the days on which this schedule applies, using full week days,
+ * with adjacent days grouped together (e.g. "Sunday to Wednesday" instead of
+ * "Sunday,Monday,Tuesday,Wednesday").
+ */
+ @Nullable
+ public static String getDaysOfWeekFull(Context context, @NonNull ScheduleInfo schedule) {
+ return getDaysSummary(context, R.string.zen_mode_trigger_summary_range_words,
+ new SimpleDateFormat("EEEE", getLocale(context)), schedule);
+ }
+
+ /**
+ * Returns an ordered summarized list of the days on which this schedule applies, with
+ * adjacent days grouped together. The formatting of each individual day of week is done with
+ * the provided {@link SimpleDateFormat}.
+ */
+ @Nullable
+ private static String getDaysSummary(Context context, @StringRes int rangeFormatResId,
+ SimpleDateFormat dayOfWeekFormat, @NonNull ScheduleInfo schedule) {
// Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or
// "Sun-Mon,Wed,Fri"
final int[] days = schedule.days;
@@ -197,19 +222,18 @@
context.getString(R.string.zen_mode_trigger_summary_divider_text));
}
- SimpleDateFormat dayFormat = new SimpleDateFormat("EEE", getLocale(context));
if (startDay == lastSeenDay) {
// last group was only one day
cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
- sb.append(dayFormat.format(cStart.getTime()));
+ sb.append(dayOfWeekFormat.format(cStart.getTime()));
} else {
// last group was a contiguous group of days, so group them together
cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
cEnd.set(Calendar.DAY_OF_WEEK, daysOfWeek[lastSeenDay]);
sb.append(context.getString(
- R.string.zen_mode_trigger_summary_range_symbol_combination,
- dayFormat.format(cStart.getTime()),
- dayFormat.format(cEnd.getTime())));
+ rangeFormatResId,
+ dayOfWeekFormat.format(cStart.getTime()),
+ dayOfWeekFormat.format(cEnd.getTime())));
}
}
}
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index dfac244..4e9d054 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -798,6 +798,18 @@
@FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
public static final int CONTENT_CHANGE_TYPE_CHECKED = 1 << 13;
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: The source node changed its
+ * expanded state which is returned by {@link AccessibilityNodeInfo#getExpandedState()}. The
+ * view changing the node's expanded state should call {@link
+ * AccessibilityNodeInfo#setExpandedState(int)} and then send this event.
+ *
+ * @see AccessibilityNodeInfo#getExpandedState()
+ * @see AccessibilityNodeInfo#setExpandedState(int)
+ */
+ @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+ public static final int CONTENT_CHANGE_TYPE_EXPANDED = 1 << 14;
+
// Speech state change types.
/** Change type for {@link #TYPE_SPEECH_STATE_CHANGE} event: another service is speaking. */
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 60ccb77..d3e7faa 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -756,6 +756,56 @@
public static final String ACTION_ARGUMENT_SCROLL_AMOUNT_FLOAT =
"android.view.accessibility.action.ARGUMENT_SCROLL_AMOUNT_FLOAT";
+ // Expanded state types.
+
+ /**
+ * Expanded state for a non-expandable element
+ *
+ * @see #getExpandedState()
+ * @see #setExpandedState(int)
+ */
+ @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+ public static final int EXPANDED_STATE_UNDEFINED = 0;
+
+ /**
+ * Expanded state for a collapsed expandable element.
+ *
+ * @see #getExpandedState()
+ * @see #setExpandedState(int)
+ */
+ @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+ public static final int EXPANDED_STATE_COLLAPSED = 1;
+
+ /**
+ * Expanded state for an expanded expandable element that can still be expanded further.
+ *
+ * @see #getExpandedState()
+ * @see #setExpandedState(int)
+ */
+ @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+ public static final int EXPANDED_STATE_PARTIAL = 2;
+
+ /**
+ * Expanded state for a expanded expandable element that cannot be expanded further.
+ *
+ * @see #getExpandedState()
+ * @see #setExpandedState(int)
+ */
+ @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+ public static final int EXPANDED_STATE_FULL = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = "EXPANDED_STATE_",
+ value = {
+ EXPANDED_STATE_UNDEFINED,
+ EXPANDED_STATE_COLLAPSED,
+ EXPANDED_STATE_PARTIAL,
+ EXPANDED_STATE_FULL,
+ })
+ public @interface ExpandedState {}
+
// Focus types.
/**
@@ -1048,6 +1098,10 @@
private int mMaxTextLength = -1;
private int mMovementGranularities;
+ // TODO(b/362782158) Initialize mExpandedState explicitly with
+ // the EXPANDED_STATE_UNDEFINED state when flagging is removed.
+ private int mExpandedState;
+
private int mTextSelectionStart = UNDEFINED_SELECTION_INDEX;
private int mTextSelectionEnd = UNDEFINED_SELECTION_INDEX;
private int mInputType = InputType.TYPE_NULL;
@@ -1931,6 +1985,47 @@
}
/**
+ * Sets the expanded state of the node.
+ *
+ * <p><strong>Note:</strong> Cannot be called from an {@link
+ * android.accessibilityservice.AccessibilityService}. This class is made immutable before being
+ * delivered to an {@link android.accessibilityservice.AccessibilityService}.
+ *
+ * @param state new expanded state of this node.
+ * @throws IllegalArgumentException If state is not one of:
+ * <ul>
+ * <li>{@link #EXPANDED_STATE_UNDEFINED}
+ * <li>{@link #EXPANDED_STATE_COLLAPSED}
+ * <li>{@link #EXPANDED_STATE_PARTIAL}
+ * <li>{@link #EXPANDED_STATE_FULL}
+ * </ul>
+ *
+ * @throws IllegalStateException If called from an AccessibilityService
+ */
+ @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+ public void setExpandedState(@ExpandedState int state) {
+ enforceValidExpandedState(state);
+ enforceNotSealed();
+ mExpandedState = state;
+ }
+
+ /**
+ * Gets the expanded state for this node.
+ *
+ * @return The expanded state, one of:
+ * <ul>
+ * <li>{@link #EXPANDED_STATE_UNDEFINED}
+ * <li>{@link #EXPANDED_STATE_COLLAPSED}
+ * <li>{@link #EXPANDED_STATE_FULL}
+ * <li>{@link #EXPANDED_STATE_PARTIAL}
+ * </ul>
+ */
+ @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API)
+ public @ExpandedState int getExpandedState() {
+ return mExpandedState;
+ }
+
+ /**
* Sets the minimum time duration between two content change events, which is used in throttling
* content change events in accessibility services.
*
@@ -4369,6 +4464,20 @@
}
}
+ private void enforceValidExpandedState(int state) {
+ if (Flags.a11yExpansionStateApi()) {
+ switch (state) {
+ case EXPANDED_STATE_UNDEFINED:
+ case EXPANDED_STATE_COLLAPSED:
+ case EXPANDED_STATE_PARTIAL:
+ case EXPANDED_STATE_FULL:
+ return;
+ default:
+ throw new IllegalArgumentException("Unknown expanded state: " + state);
+ }
+ }
+ }
+
/**
* Enforces that this instance is not sealed.
*
@@ -4623,6 +4732,11 @@
if (mChecked != DEFAULT.mChecked) {
nonDefaultFields |= bitAt(fieldIndex);
}
+ fieldIndex++;
+ if (mExpandedState != DEFAULT.mExpandedState) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+
int totalFields = fieldIndex;
parcel.writeLong(nonDefaultFields);
@@ -4794,6 +4908,9 @@
if (isBitSet(nonDefaultFields, fieldIndex++)) {
parcel.writeInt(mChecked);
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mExpandedState);
+ }
if (DEBUG) {
fieldIndex--;
@@ -4883,6 +5000,7 @@
mLeashedParent = other.mLeashedParent;
mLeashedParentNodeId = other.mLeashedParentNodeId;
mChecked = other.mChecked;
+ mExpandedState = other.mExpandedState;
}
private void initCopyInfos(AccessibilityNodeInfo other) {
@@ -5075,6 +5193,9 @@
if (isBitSet(nonDefaultFields, fieldIndex++)) {
mChecked = parcel.readInt();
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mExpandedState = parcel.readInt();
+ }
mSealed = sealed;
}
@@ -5249,6 +5370,26 @@
}
}
+ private static String getExpandedStateSymbolicName(int state) {
+ if (Flags.a11yExpansionStateApi()) {
+ switch (state) {
+ case EXPANDED_STATE_UNDEFINED:
+ return "EXPANDED_STATE_UNDEFINED";
+ case EXPANDED_STATE_COLLAPSED:
+ return "EXPANDED_STATE_COLLAPSED";
+ case EXPANDED_STATE_PARTIAL:
+ return "EXPANDED_STATE_PARTIAL";
+ case EXPANDED_STATE_FULL:
+ return "EXPANDED_STATE_FULL";
+ default:
+ throw new IllegalArgumentException("Unknown expanded state: " + state);
+ }
+ } else {
+ // TODO(b/362782158) Remove when flag is removed.
+ return "";
+ }
+ }
+
private static boolean canPerformRequestOverConnection(int connectionId,
int windowId, long accessibilityNodeId) {
final boolean hasWindowId = windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
@@ -5346,6 +5487,7 @@
builder.append("; containerTitle: ").append(mContainerTitle);
builder.append("; viewIdResName: ").append(mViewIdResourceName);
builder.append("; uniqueId: ").append(mUniqueId);
+ builder.append("; expandedState: ").append(getExpandedStateSymbolicName(mExpandedState));
builder.append("; checkable: ").append(isCheckable());
builder.append("; checked: ").append(isChecked());
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 05dc910..22eec29 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -49,6 +49,7 @@
ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS(
Flags::enableCaptionCompatInsetForceConsumptionAlways, true),
ENABLE_CASCADING_WINDOWS(Flags::enableCascadingWindows, true),
+ ENABLE_TILE_RESIZING(Flags::enableTileResizing, true),
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(
Flags::enableDesktopWindowingWallpaperActivity, true),
ENABLE_DESKTOP_WINDOWING_MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 7aca535..732c316 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5348,6 +5348,8 @@
<string name="zen_mode_trigger_summary_divider_text">,\u0020</string>
<!-- [CHAR LIMIT=40] General template for a symbolic start - end range in a text summary, used for the trigger description of a Zen mode -->
<string name="zen_mode_trigger_summary_range_symbol_combination"><xliff:g id="start" example="Sun">%1$s</xliff:g> - <xliff:g id="end" example="Thu">%2$s</xliff:g></string>
+ <!-- [CHAR LIMIT=40] General template for a start - end range in a text summary, used for the trigger description of a Zen mode -->
+ <string name="zen_mode_trigger_summary_range_words"><xliff:g id="start" example="Sunday">%1$s</xliff:g> to <xliff:g id="end" example="Thursday">%2$s</xliff:g></string>
<!-- [CHAR LIMIT=40] Event-based rule calendar option value for any calendar, used for the trigger description of a Zen mode -->
<string name="zen_mode_trigger_event_calendar_any">Any calendar</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ecc3afe..4dbeb2f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2657,6 +2657,7 @@
<java-symbol type="string" name="zen_mode_implicit_deactivated" />
<java-symbol type="string" name="zen_mode_trigger_summary_divider_text" />
<java-symbol type="string" name="zen_mode_trigger_summary_range_symbol_combination" />
+ <java-symbol type="string" name="zen_mode_trigger_summary_range_words" />
<java-symbol type="string" name="zen_mode_trigger_event_calendar_any" />
<java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
index 013117e..1721e1e 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java
@@ -34,6 +34,7 @@
import android.os.Parcel;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
@@ -87,6 +88,8 @@
TEST_EDITOR_INFO.targetInputMethodUser = UserHandle.of(TEST_USER_ID);
}
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
/**
* Makes sure that {@code null} {@link EditorInfo#targetInputMethodUser} can be copied via
* {@link Parcel}.
@@ -522,7 +525,9 @@
info.setSupportedHandwritingGestures(Arrays.asList(SelectGesture.class));
info.setSupportedHandwritingGesturePreviews(
Stream.of(SelectGesture.class).collect(Collectors.toSet()));
- if (Flags.editorinfoHandwritingEnabled()) {
+ final boolean isStylusHandwritingEnabled =
+ mFlagsValueProvider.getBoolean(Flags.FLAG_EDITORINFO_HANDWRITING_ENABLED);
+ if (isStylusHandwritingEnabled) {
info.setStylusHandwritingEnabled(true);
}
info.packageName = "android.view.inputmethod";
@@ -548,8 +553,7 @@
+ "prefix2: hintLocales=[en,es,zh]\n"
+ "prefix2: supportedHandwritingGestureTypes=SELECT\n"
+ "prefix2: supportedHandwritingGesturePreviewTypes=SELECT\n"
- + "prefix2: isStylusHandwritingEnabled="
- + Flags.editorinfoHandwritingEnabled() + "\n"
+ + "prefix2: isStylusHandwritingEnabled=" + isStylusHandwritingEnabled + "\n"
+ "prefix2: contentMimeTypes=[image/png]\n"
+ "prefix2: targetInputMethodUserId=10\n");
}
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
index 61bf137..44b2d90 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
@@ -26,6 +26,8 @@
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.os.Parcel;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,8 +45,9 @@
public class InputMethodInfoTest {
@Rule
- public SetFlagsRule mSetFlagsRule =
- new SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+ public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
@Test
public void testEqualsAndHashCode() throws Exception {
@@ -70,7 +73,7 @@
assertThat(imi.supportsInlineSuggestionsWithTouchExploration(), is(false));
assertThat(imi.supportsStylusHandwriting(), is(false));
assertThat(imi.createStylusHandwritingSettingsActivityIntent(), equalTo(null));
- if (Flags.imeSwitcherRevampApi()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_IME_SWITCHER_REVAMP_API)) {
assertThat(imi.createImeLanguageSettingsActivityIntent(), equalTo(null));
}
}
@@ -121,9 +124,8 @@
}
@Test
+ @EnableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_IME)
public void testIsVirtualDeviceOnly() throws Exception {
- mSetFlagsRule.enableFlags(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_IME);
-
final InputMethodInfo imi = buildInputMethodForTest(R.xml.ime_meta_virtual_device_only);
assertThat(imi.isVirtualDeviceOnly(), is(true));
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
index da202b6..930e03d 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
@@ -46,7 +46,7 @@
// The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest:
// See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo,
// and assertAccessibilityNodeInfoCleared in that class.
- private static final int NUM_MARSHALLED_PROPERTIES = 45;
+ private static final int NUM_MARSHALLED_PROPERTIES = 46;
/**
* The number of properties that are purposely not marshalled
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
index b6db6d9..61c09f2 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
@@ -29,6 +29,8 @@
static_libs: [
"WindowManager-Shell",
"platform-screenshot-diff-core",
+ "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot
+ "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib)
],
asset_dirs: ["goldens/robolectric"],
manifest: "AndroidManifestRobolectric.xml",
@@ -63,6 +65,8 @@
],
static_libs: [
"WindowManager-Shell",
+ "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot
+ "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib)
"junit",
"androidx.test.runner",
"androidx.test.rules",
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
index b4bdaea..72d0d5e4 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
@@ -18,6 +18,7 @@
<application android:debuggable="true" android:supportsRtl="true">
<activity
android:name="platform.test.screenshot.ScreenshotActivity"
+ android:theme="@style/Theme.PlatformUi.Screenshot"
android:exported="true">
</activity>
</application>
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
index 736bca7..5b429c0 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
index 736bca7..6028fa2 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
index f09969d..8cf3ce9 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
@@ -15,10 +15,12 @@
*/
package com.android.wm.shell.bubbles
+import android.graphics.Color
import android.view.LayoutInflater
+import android.view.ViewGroup
+import com.android.wm.shell.R
import com.android.wm.shell.shared.bubbles.BubblePopupView
import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager
-import com.android.wm.shell.R
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,6 +50,10 @@
fun bubblesEducation() {
screenshotRule.screenshotTest("bubbles_education") { activity ->
activity.actionBar?.hide()
+ // Set the background color of the activity to be something not from the theme to
+ // ensure good contrast between the education view and the background
+ val rootView = activity.window.decorView.findViewById(android.R.id.content) as ViewGroup
+ rootView.setBackgroundColor(Color.RED)
val view =
LayoutInflater.from(activity)
.inflate(R.layout.bubble_bar_stack_education, null) as BubblePopupView
diff --git a/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml b/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml
new file mode 100644
index 0000000..ce2e8be
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tiling_split_divider.xml
@@ -0,0 +1,37 @@
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<com.android.wm.shell.windowdecor.tiling.TilingDividerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:id="@+id/divider_bar">
+
+ <com.android.wm.shell.common.split.DividerHandleView
+ android:id="@+id/docked_divider_handle"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:layout_gravity="center"
+ android:contentDescription="@string/accessibility_divider"
+ android:background="@null"/>
+
+ <com.android.wm.shell.common.split.DividerRoundedCorner
+ android:id="@+id/docked_divider_rounded_corner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+
+</com.android.wm.shell.windowdecor.tiling.TilingDividerView>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
index 6c83d88..eb7ef14 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
@@ -63,8 +63,19 @@
* @param currentBounds {@link Rect} of the current animation bounds.
* @param fraction progress of the animation ranged from 0f to 1f.
*/
- public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
- Rect currentBounds, float fraction);
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ Rect currentBounds, float fraction) {}
+
+ /**
+ * Animates the internal {@link #mLeash} by a given fraction for a config-at-end transition.
+ * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
+ * call apply on this transaction, it should be applied on the caller side.
+ * @param scale scaling to apply onto the overlay.
+ * @param fraction progress of the animation ranged from 0f to 1f.
+ * @param endBounds the final bounds PiP is animating into.
+ */
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ float scale, float fraction, Rect endBounds) {}
/** A {@link PipContentOverlay} uses solid color. */
public static final class PipColorOverlay extends PipContentOverlay {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
index bdbd4cf..6c04e2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java
@@ -103,7 +103,8 @@
mHoveringHeight = mHeight > mWidth ? ((int) (mHeight * 1.5f)) : mHeight;
}
- void setIsLeftRightSplit(boolean isLeftRightSplit) {
+ /** sets whether it's a left/right or top/bottom split */
+ public void setIsLeftRightSplit(boolean isLeftRightSplit) {
mIsLeftRightSplit = isLeftRightSplit;
updateDimens();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
index 834c15d..d5aaf75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java
@@ -98,7 +98,11 @@
return false;
}
- void setIsLeftRightSplit(boolean isLeftRightSplit) {
+ /**
+ * Set whether the rounded corner is for a left/right split.
+ * @param isLeftRightSplit whether it's a left/right split or top/bottom split.
+ */
+ public void setIsLeftRightSplit(boolean isLeftRightSplit) {
mIsLeftRightSplit = isLeftRightSplit;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 7f54786..0942e05 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -130,6 +130,7 @@
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController;
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel;
import dagger.Binds;
import dagger.Lazy;
@@ -649,7 +650,8 @@
InteractionJankMonitor interactionJankMonitor,
InputManager inputManager,
FocusTransitionObserver focusTransitionObserver,
- DesktopModeEventLogger desktopModeEventLogger) {
+ DesktopModeEventLogger desktopModeEventLogger,
+ DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
dragAndDropController, transitions, keyguardManager,
@@ -661,8 +663,32 @@
desktopModeLoggerTransitionObserver, launchAdjacentController,
recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
recentTasksController.orElse(null), interactionJankMonitor, mainHandler,
- inputManager, focusTransitionObserver,
- desktopModeEventLogger);
+ inputManager, focusTransitionObserver, desktopModeEventLogger,
+ desktopTilingDecorViewModel);
+ }
+
+ @WMSingleton
+ @Provides
+ static DesktopTilingDecorViewModel provideDesktopTilingViewModel(Context context,
+ DisplayController displayController,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ SyncTransactionQueue syncQueue,
+ Transitions transitions,
+ ShellTaskOrganizer shellTaskOrganizer,
+ ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
+ ReturnToDragStartAnimator returnToDragStartAnimator,
+ @DynamicOverride DesktopRepository desktopRepository) {
+ return new DesktopTilingDecorViewModel(
+ context,
+ displayController,
+ rootTaskDisplayAreaOrganizer,
+ syncQueue,
+ transitions,
+ shellTaskOrganizer,
+ toggleResizeDesktopTaskTransitionHandler,
+ returnToDragStartAnimator,
+ desktopRepository
+ );
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index bc78e43..eec2ba5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -118,6 +118,7 @@
import com.android.wm.shell.transition.OneShotRemoteHandler
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
@@ -125,6 +126,7 @@
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.requestingImmersive
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
import java.io.PrintWriter
import java.util.Optional
import java.util.concurrent.Executor
@@ -163,6 +165,7 @@
private val inputManager: InputManager,
private val focusTransitionObserver: FocusTransitionObserver,
private val desktopModeEventLogger: DesktopModeEventLogger,
+ private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel,
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
@@ -237,6 +240,7 @@
override fun onAnimationStateChanged(running: Boolean) {
logV("Recents animation state changed running=%b", running)
recentsAnimationRunning = running
+ desktopTilingDecorViewModel.onOverviewAnimationStateChange(running)
}
}
)
@@ -492,6 +496,7 @@
taskInfo: RunningTaskInfo,
): ((IBinder) -> Unit)? {
val taskId = taskInfo.taskId
+ desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId)
if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
removeWallpaperActivity(wct)
}
@@ -532,6 +537,7 @@
/** Move a task with given `taskId` to fullscreen */
fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) {
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
+ desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, taskId)
moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource)
}
}
@@ -539,6 +545,7 @@
/** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */
fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) {
getFocusedFreeformTask(displayId)?.let {
+ desktopTilingDecorViewModel.removeTaskIfTiled(displayId, it.taskId)
moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource)
}
}
@@ -546,6 +553,7 @@
/** Move a desktop app to split screen. */
fun moveToSplit(task: RunningTaskInfo) {
logV( "moveToSplit taskId=%s", task.taskId)
+ desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
val wct = WindowContainerTransaction()
wct.setBounds(task.token, Rect())
// Rather than set windowing mode to multi-window at task level, set it to
@@ -643,6 +651,11 @@
@JvmOverloads
fun moveTaskToFront(taskInfo: RunningTaskInfo, remoteTransition: RemoteTransition? = null) {
logV("moveTaskToFront taskId=%s", taskInfo.taskId)
+ // If a task is tiled, another task should be brought to foreground with it so let
+ // tiling controller handle the request.
+ if (desktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo)) {
+ return
+ }
val wct = WindowContainerTransaction()
wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
@@ -804,13 +817,13 @@
} else {
// Save current bounds so that task can be restored back to original bounds if necessary
// and toggle to the stable bounds.
+ desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
}
-
val shouldRestoreToSnap =
isMaximized && isTaskSnappedToHalfScreen(taskInfo, destinationBounds)
@@ -918,7 +931,20 @@
position: SnapPosition,
resizeTrigger: ResizeTrigger,
motionEvent: MotionEvent?,
+ desktopWindowDecoration: DesktopModeWindowDecoration,
) {
+ if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) {
+ val isTiled = desktopTilingDecorViewModel.snapToHalfScreen(
+ taskInfo,
+ desktopWindowDecoration,
+ position,
+ currentDragBounds,
+ )
+ if (isTiled) {
+ taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true)
+ }
+ return
+ }
val destinationBounds = getSnapBounds(taskInfo, position)
desktopModeEventLogger.logTaskResizingEnded(
resizeTrigger,
@@ -938,7 +964,7 @@
taskSurface,
startBounds = currentDragBounds,
endBounds = destinationBounds,
- isResizable = taskInfo.isResizeable
+ isResizable = taskInfo.isResizeable,
)
}
return
@@ -958,6 +984,7 @@
currentDragBounds: Rect,
dragStartBounds: Rect,
motionEvent: MotionEvent,
+ desktopModeWindowDecoration: DesktopModeWindowDecoration,
) {
releaseVisualIndicator()
if (!taskInfo.isResizeable && DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()) {
@@ -992,6 +1019,7 @@
position,
resizeTrigger,
motionEvent,
+ desktopModeWindowDecoration,
)
}
}
@@ -1499,7 +1527,11 @@
addPendingMinimizeTransition(transition, taskIdToMinimize)
return wct
}
- return if (wct.isEmpty) null else wct
+ if (!wct.isEmpty) {
+ desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
+ return wct
+ }
+ return null
}
private fun handleFullscreenTaskLaunch(
@@ -1565,6 +1597,7 @@
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
taskRepository.addClosingTask(task.displayId, task.taskId)
+ desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId)
}
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
@@ -1812,6 +1845,7 @@
taskBounds: Rect
) {
if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return
+ desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
updateVisualIndicator(taskInfo, taskSurface, inputX, taskBounds.top.toFloat(),
DragStartState.FROM_FREEFORM)
}
@@ -1861,6 +1895,7 @@
validDragArea: Rect,
dragStartBounds: Rect,
motionEvent: MotionEvent,
+ desktopModeWindowDecoration: DesktopModeWindowDecoration,
) {
if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
return
@@ -1887,6 +1922,7 @@
currentDragBounds,
dragStartBounds,
motionEvent,
+ desktopModeWindowDecoration,
)
}
IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
@@ -1897,6 +1933,7 @@
currentDragBounds,
dragStartBounds,
motionEvent,
+ desktopModeWindowDecoration,
)
}
IndicatorType.NO_INDICATOR -> {
@@ -2090,6 +2127,7 @@
// TODO(b/366397912): Support full multi-user mode in Windowing.
override fun onUserChanged(newUserId: Int, userContext: Context) {
userId = newUserId
+ desktopTilingDecorViewModel.onUserChange()
}
/** Called when a task's info changes. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
index 5381a62..740b9af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java
@@ -23,6 +23,7 @@
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
+import android.content.pm.ActivityInfo;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -33,10 +34,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+import com.android.wm.shell.pip2.phone.PipAppIconOverlay;
import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.pip.PipContentOverlay;
/**
* Animator that handles bounds animations for entering PIP.
@@ -59,6 +63,10 @@
private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
+ Matrix mTransformTensor = new Matrix();
+ final float[] mMatrixTmp = new float[9];
+ @Nullable private PipContentOverlay mContentOverlay;
+
// Internal state representing initial transform - cached to avoid recalculation.
private final PointF mInitScale = new PointF();
@@ -67,9 +75,6 @@
private final PointF mInitActivityScale = new PointF();
private final PointF mInitActivityPos = new PointF();
- Matrix mTransformTensor = new Matrix();
- final float[] mMatrixTmp = new float[9];
-
public PipEnterAnimator(Context context,
@NonNull SurfaceControl leash,
SurfaceControl.Transaction startTransaction,
@@ -161,10 +166,15 @@
mRectEvaluator.evaluate(fraction, initCrop, endCrop);
tx.setCrop(mLeash, mAnimatedRect);
+ mTransformTensor.reset();
mTransformTensor.setScale(scaleX, scaleY);
mTransformTensor.postTranslate(posX, posY);
mTransformTensor.postRotate(degrees);
tx.setMatrix(mLeash, mTransformTensor, mMatrixTmp);
+
+ if (mContentOverlay != null) {
+ mContentOverlay.onAnimationUpdate(tx, 1f / scaleX, fraction, mEndBounds);
+ }
}
// no-ops
@@ -200,4 +210,48 @@
}
PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop);
}
+
+ /**
+ * Initializes and attaches an app icon overlay on top of the PiP layer.
+ */
+ public void setAppIconContentOverlay(Context context, Rect appBounds, Rect destinationBounds,
+ ActivityInfo activityInfo, int appIconSizePx) {
+ reattachAppIconOverlay(
+ new PipAppIconOverlay(context, appBounds, destinationBounds,
+ new IconProvider(context).getIcon(activityInfo), appIconSizePx));
+ }
+
+ private void reattachAppIconOverlay(PipAppIconOverlay overlay) {
+ final SurfaceControl.Transaction tx =
+ mSurfaceControlTransactionFactory.getTransaction();
+ if (mContentOverlay != null) {
+ mContentOverlay.detach(tx);
+ }
+ mContentOverlay = overlay;
+ mContentOverlay.attach(tx, mLeash);
+ }
+
+ /**
+ * Clears the {@link #mContentOverlay}, this should be done after the content overlay is
+ * faded out.
+ */
+ public void clearAppIconOverlay() {
+ if (mContentOverlay == null) {
+ return;
+ }
+ SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+ mContentOverlay.detach(tx);
+ mContentOverlay = null;
+ }
+
+ /**
+ * @return the app icon overlay leash; null if no overlay is attached.
+ */
+ @Nullable
+ public SurfaceControl getContentOverlayLeash() {
+ if (mContentOverlay == null) {
+ return null;
+ }
+ return mContentOverlay.getLeash();
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
new file mode 100644
index 0000000..b4cf890
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.shared.pip.PipContentOverlay;
+
+/** A {@link PipContentOverlay} shows app icon on solid color background. */
+public final class PipAppIconOverlay extends PipContentOverlay {
+ private static final String TAG = PipAppIconOverlay.class.getSimpleName();
+ // The maximum size for app icon in pixel.
+ private static final int MAX_APP_ICON_SIZE_DP = 72;
+
+ private final Context mContext;
+ private final int mAppIconSizePx;
+ private final Rect mAppBounds;
+ private final int mOverlayHalfSize;
+ private final Matrix mTmpTransform = new Matrix();
+ private final float[] mTmpFloat9 = new float[9];
+
+ private Bitmap mBitmap;
+
+ public PipAppIconOverlay(Context context, Rect appBounds, Rect destinationBounds,
+ Drawable appIcon, int appIconSizePx) {
+ mContext = context;
+ final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
+ MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics());
+ mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx);
+
+ final int overlaySize = getOverlaySize(appBounds, destinationBounds);
+ mOverlayHalfSize = overlaySize >> 1;
+
+ // When the activity is in the secondary split, make sure the scaling center is not
+ // offset.
+ mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height());
+
+ mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
+ prepareAppIconOverlay(appIcon);
+ mLeash = new SurfaceControl.Builder()
+ .setCallsite(TAG)
+ .setName(LAYER_NAME)
+ .build();
+ }
+
+ /**
+ * Returns the size of the app icon overlay.
+ *
+ * In order to have the overlay always cover the pip window during the transition,
+ * the overlay will be drawn with the max size of the start and end bounds in different
+ * rotation.
+ */
+ public static int getOverlaySize(Rect appBounds, Rect destinationBounds) {
+ final int appWidth = appBounds.width();
+ final int appHeight = appBounds.height();
+
+ return Math.max(Math.max(appWidth, appHeight),
+ Math.max(destinationBounds.width(), destinationBounds.height())) + 1;
+ }
+
+ @Override
+ public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
+ tx.show(mLeash);
+ tx.setLayer(mLeash, Integer.MAX_VALUE);
+ tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
+ tx.setAlpha(mLeash, 0f);
+ tx.reparent(mLeash, parentLeash);
+ tx.apply();
+ }
+
+ @Override
+ public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
+ float scale, float fraction, Rect endBounds) {
+ mTmpTransform.reset();
+ // Scale back the bitmap with the pivot at parent origin
+ mTmpTransform.setScale(scale, scale);
+ // We are negative-cropping away from the final bounds crop in config-at-end enter PiP;
+ // this means that the overlay shift depends on the final bounds.
+ // Note: translation is also dependent on the scaling of the parent.
+ mTmpTransform.postTranslate(endBounds.width() / 2f - mOverlayHalfSize * scale,
+ endBounds.height() / 2f - mOverlayHalfSize * scale);
+ atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
+ .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
+ }
+
+
+
+ @Override
+ public void detach(SurfaceControl.Transaction tx) {
+ super.detach(tx);
+ if (mBitmap != null && !mBitmap.isRecycled()) {
+ mBitmap.recycle();
+ }
+ }
+
+ private void prepareAppIconOverlay(Drawable appIcon) {
+ final Canvas canvas = new Canvas();
+ canvas.setBitmap(mBitmap);
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.colorBackground });
+ try {
+ int colorAccent = ta.getColor(0, 0);
+ canvas.drawRGB(
+ Color.red(colorAccent),
+ Color.green(colorAccent),
+ Color.blue(colorAccent));
+ } finally {
+ ta.recycle();
+ }
+ final Rect appIconBounds = new Rect(
+ mOverlayHalfSize - mAppIconSizePx / 2,
+ mOverlayHalfSize - mAppIconSizePx / 2,
+ mOverlayHalfSize + mAppIconSizePx / 2,
+ mOverlayHalfSize + mAppIconSizePx / 2);
+ appIcon.setBounds(appIconBounds);
+ appIcon.draw(canvas);
+ mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 0427294..9a93371 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -456,6 +456,10 @@
}
}
+ private void setLauncherAppIconSize(int iconSizePx) {
+ mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx);
+ }
+
/**
* The interface for calls from outside the Shell, within the host process.
*/
@@ -571,7 +575,10 @@
}
@Override
- public void setLauncherAppIconSize(int iconSizePx) {}
+ public void setLauncherAppIconSize(int iconSizePx) {
+ executeRemoteCallWithTaskPermission(mController, "setLauncherAppIconSize",
+ (controller) -> controller.setLauncherAppIconSize(iconSizePx));
+ }
@Override
public void setPipAnimationListener(IPipAnimationListener listener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index b286211..e90b32c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -30,9 +30,6 @@
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.PictureInPictureParams;
@@ -362,33 +359,19 @@
animator.setEnterStartState(pipChange, pipActivityChange);
animator.onEnterAnimationUpdate(1.0f /* fraction */, startTransaction);
startTransaction.apply();
+
+ if (swipePipToHomeOverlay != null) {
+ // fadeout the overlay if needed.
+ startOverlayFadeoutAnimation(swipePipToHomeOverlay, () -> {
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ tx.remove(swipePipToHomeOverlay);
+ tx.apply();
+ });
+ }
finishInner();
return true;
}
- private void startOverlayFadeoutAnimation() {
- ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f);
- animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
- tx.remove(mPipTransitionState.getSwipePipToHomeOverlay());
- tx.apply();
-
- // We have fully completed enter-PiP animation after the overlay is gone.
- mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
- }
- });
- animator.addUpdateListener(animation -> {
- float alpha = (float) animation.getAnimatedValue();
- SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
- tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply();
- });
- animator.start();
- }
-
private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -405,15 +388,18 @@
return false;
}
- Rect endBounds = pipChange.getEndAbsBounds();
- SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
- Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
+ final Rect startBounds = pipChange.getStartAbsBounds();
+ final Rect endBounds = pipChange.getEndAbsBounds();
- Rect sourceRectHint = null;
- if (pipChange.getTaskInfo() != null
- && pipChange.getTaskInfo().pictureInPictureParams != null) {
- sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint();
- }
+ final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
+ final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params);
+
+ final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds,
+ endBounds);
+
+ final SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+ final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint)
+ : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio);
// For opening type transitions, if there is a change of mode TO_FRONT/OPEN,
// make sure that change has alpha of 1f, since it's init state might be set to alpha=0f
@@ -441,14 +427,36 @@
}
PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
- startTransaction, finishTransaction, endBounds, sourceRectHint, delta);
+ startTransaction, finishTransaction, endBounds, adjustedSourceRectHint, delta);
+ if (sourceRectHint == null) {
+ // update the src-rect-hint in params in place, to set up initial animator transform.
+ params.getSourceRectHint().set(adjustedSourceRectHint);
+ animator.setAppIconContentOverlay(
+ mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo,
+ mPipBoundsState.getLauncherState().getAppIconSizePx());
+ }
animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange,
pipActivityChange));
- animator.setAnimationEndCallback(this::finishInner);
+ animator.setAnimationEndCallback(() -> {
+ if (animator.getContentOverlayLeash() != null) {
+ startOverlayFadeoutAnimation(animator.getContentOverlayLeash(),
+ animator::clearAppIconOverlay);
+ }
+ finishInner();
+ });
animator.start();
return true;
}
+ private void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash,
+ @NonNull Runnable onAnimationEnd) {
+ PipAlphaAnimator animator = new PipAlphaAnimator(mContext, overlayLeash,
+ null /* startTx */, PipAlphaAnimator.FADE_OUT);
+ animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
+ animator.setAnimationEndCallback(onAnimationEnd);
+ animator.start();
+ }
+
private void handleBoundsTypeFixedRotation(TransitionInfo.Change pipTaskChange,
TransitionInfo.Change pipActivityChange, int endRotation) {
final Rect endBounds = pipTaskChange.getEndAbsBounds();
@@ -696,9 +704,7 @@
private void finishInner() {
finishTransition(null /* tx */);
- if (mPipTransitionState.getSwipePipToHomeOverlay() != null) {
- startOverlayFadeoutAnimation();
- } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
+ if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
// If we were entering PiP (i.e. playing the animation) with a valid srcRectHint,
// and then we get a signal on client finishing its draw after the transition
// has ended, then we have fully entered PiP.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index fde01ee..3946b61 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -290,9 +290,11 @@
final Resources res = mResult.mRootView.getResources();
mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */,
- new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res),
- getResizeHandleEdgeInset(res), getFineResizeCornerSize(res),
- getLargeResizeCornerSize(res)), touchSlop);
+ new Size(mResult.mWidth, mResult.mHeight),
+ getResizeEdgeHandleSize(res),
+ getResizeHandleEdgeInset(res), getFineResizeCornerSize(res),
+ getLargeResizeCornerSize(res), DragResizeWindowGeometry.DisabledEdge.NONE),
+ touchSlop);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index f404326..a3324cc6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -187,7 +187,7 @@
new ExclusionRegionListenerImpl();
private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId;
- private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
+ private final DragEventListenerImpl mDragEventListener = new DragEventListenerImpl();
private final InputMonitorFactory mInputMonitorFactory;
private TaskOperations mTaskOperations;
private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
@@ -613,7 +613,8 @@
decoration.mTaskInfo.configuration.windowConfiguration.getBounds(),
left ? SnapPosition.LEFT : SnapPosition.RIGHT,
resizeTrigger,
- motionEvent);
+ motionEvent,
+ mWindowDecorByTaskId.get(taskId));
}
decoration.closeHandleMenu();
@@ -1072,7 +1073,8 @@
taskInfo, decoration.mTaskSurface, position,
new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
newTaskBounds, decoration.calculateValidDragArea(),
- new Rect(mOnDragStartInitialBounds), e);
+ new Rect(mOnDragStartInitialBounds), e,
+ mWindowDecorByTaskId.get(taskInfo.taskId));
if (touchingButton && !mHasLongClicked) {
// We need the input event to not be consumed here to end the ripple
// effect on the touched button. We will reset drag state in the ensuing
@@ -1524,7 +1526,7 @@
mTaskOrganizer,
windowDecoration,
mDisplayController,
- mDragStartListener,
+ mDragEventListener,
mTransitions,
mInteractionJankMonitor,
mTransactionFactory,
@@ -1667,13 +1669,18 @@
}
}
- private class DragStartListenerImpl
- implements DragPositioningCallbackUtility.DragStartListener {
+ private class DragEventListenerImpl
+ implements DragPositioningCallbackUtility.DragEventListener {
@Override
public void onDragStart(int taskId) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
decoration.closeHandleMenu();
}
+
+ @Override
+ public void onDragMove(int taskId) {
+
+ }
}
/**
@@ -1767,7 +1774,7 @@
ShellTaskOrganizer taskOrganizer,
DesktopModeWindowDecoration windowDecoration,
DisplayController displayController,
- DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ DragPositioningCallbackUtility.DragEventListener dragEventListener,
Transitions transitions,
InteractionJankMonitor interactionJankMonitor,
Supplier<SurfaceControl.Transaction> transactionFactory,
@@ -1777,7 +1784,7 @@
taskOrganizer,
windowDecoration,
displayController,
- dragStartListener,
+ dragEventListener,
transitions,
interactionJankMonitor,
handler)
@@ -1786,7 +1793,7 @@
transitions,
windowDecoration,
displayController,
- dragStartListener,
+ dragEventListener,
transactionFactory);
if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 8865112..dc27cfe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -27,14 +27,18 @@
import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION;
import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS;
+
import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset;
+import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -155,6 +159,8 @@
private DragResizeInputListener mDragResizeListener;
private Runnable mCurrentViewHostRunnable = null;
private RelayoutParams mRelayoutParams = new RelayoutParams();
+ private DisabledEdge mDisabledResizingEdge =
+ NONE;
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
new WindowDecoration.RelayoutResult<>();
private final Runnable mViewHostRunnable =
@@ -329,6 +335,24 @@
mOnToSplitscreenClickListener = listener;
}
+ /**
+ * Adds a drag resize observer that gets notified on the task being drag resized.
+ *
+ * @param dragResizeListener The observing object to be added.
+ */
+ public void addDragResizeListener(DragEventListener dragResizeListener) {
+ mTaskDragResizer.addDragEventListener(dragResizeListener);
+ }
+
+ /**
+ * Removes an already existing drag resize observer.
+ *
+ * @param dragResizeListener observer to be removed.
+ */
+ public void removeDragResizeListener(DragEventListener dragResizeListener) {
+ mTaskDragResizer.removeDragEventListener(dragResizeListener);
+ }
+
/** Registers a listener to be called when the decoration's new window action is triggered. */
void setOnNewWindowClickListener(Function0<Unit> listener) {
mOnNewWindowClickListener = listener;
@@ -386,6 +410,24 @@
}
}
+ /**
+ * Disables resizing for the given edge.
+ *
+ * @param disabledResizingEdge edge to disable.
+ * @param shouldDelayUpdate whether the update should be executed immediately or delayed.
+ */
+ public void updateDisabledResizingEdge(
+ DragResizeWindowGeometry.DisabledEdge disabledResizingEdge, boolean shouldDelayUpdate) {
+ mDisabledResizingEdge = disabledResizingEdge;
+ final boolean inFullImmersive = mDesktopRepository
+ .isTaskInFullImmersiveState(mTaskInfo.taskId);
+ if (shouldDelayUpdate) {
+ return;
+ }
+ updateDragResizeListener(mDecorationContainerSurface, inFullImmersive);
+ }
+
+
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
@@ -645,7 +687,8 @@
new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius,
new Size(mResult.mWidth, mResult.mHeight),
getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
- getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop)
+ getFineResizeCornerSize(res), getLargeResizeCornerSize(res),
+ mDisabledResizingEdge), touchSlop)
|| !mTaskInfo.positionInParent.equals(mPositionInParent)) {
updateExclusionRegion(inFullImmersive);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
index 01bb7f7..d36fc12 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java
@@ -42,7 +42,7 @@
*
* All touch events must be passed through this class to track a drag event.
*/
-class DragDetector {
+public class DragDetector {
private final MotionEventHandler mEventHandler;
private final PointF mInputDownPoint = new PointF();
@@ -55,7 +55,14 @@
private boolean mResultOfDownAction;
- DragDetector(@NonNull MotionEventHandler eventHandler, long holdToDragMinDurationMs,
+ /**
+ * Initialises a drag detector.
+ *
+ * @param eventHandler drag event handler.
+ * @param holdToDragMinDurationMs hold to drag duration.
+ * @param touchSlop touch slope threshold.
+ */
+ public DragDetector(@NonNull MotionEventHandler eventHandler, long holdToDragMinDurationMs,
int touchSlop) {
resetState();
mEventHandler = eventHandler;
@@ -69,7 +76,7 @@
* @return the result returned by {@link #mEventHandler}, or the result when
* {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
*/
- boolean onMotionEvent(MotionEvent ev) {
+ public boolean onMotionEvent(MotionEvent ev) {
return onMotionEvent(null /* view */, ev);
}
@@ -79,7 +86,7 @@
* @return the result returned by {@link #mEventHandler}, or the result when
* {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
*/
- boolean onMotionEvent(View v, MotionEvent ev) {
+ public boolean onMotionEvent(View v, MotionEvent ev) {
final boolean isTouchScreen =
(ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
if (!isTouchScreen) {
@@ -190,7 +197,16 @@
mDidHoldForMinDuration = false;
}
- interface MotionEventHandler {
+ /**
+ * Interface to be implemented by the class using the DragDetector for callback.
+ */
+ public interface MotionEventHandler {
+ /**
+ * Called back when drag is detected to notify the implementing class to handle drag events.
+ * @param v view on which the input arrived.
+ * @param ev motion event that resulted in drag.
+ * @return whether this was a drag event or not.
+ */
boolean handleMotionEvent(@Nullable View v, MotionEvent ev);
}
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 78e7962..8eced3e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -208,7 +208,17 @@
return result;
}
- private static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth,
+ /**
+ * Checks whether the new task bounds exceed the allowed width.
+ *
+ * @param repositionedWidth task width after repositioning.
+ * @param startingWidth task width before repositioning.
+ * @param maxResizeBounds stable bounds for display.
+ * @param displayController display controller for the task being checked.
+ * @param windowDecoration contains decor info and helpers for the task.
+ * @return whether the task is exceeding any of the width constrains, minimum or maximum.
+ */
+ public static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth,
Rect maxResizeBounds, DisplayController displayController,
WindowDecoration windowDecoration) {
boolean isSizeIncreasing = (repositionedWidth - startingWidth) > 0;
@@ -223,7 +233,17 @@
&& repositionedWidth > maxResizeBounds.width() && isSizeIncreasing;
}
- private static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight,
+ /**
+ * Checks whether the new task bounds exceed the allowed height.
+ *
+ * @param repositionedHeight task's height after repositioning.
+ * @param startingHeight task's height before repositioning.
+ * @param maxResizeBounds stable bounds for display.
+ * @param displayController display controller for the task being checked.
+ * @param windowDecoration contains decor info and helpers for the task.
+ * @return whether the task is exceeding any of the height constrains, minimum or maximum.
+ */
+ public static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight,
Rect maxResizeBounds, DisplayController displayController,
WindowDecoration windowDecoration) {
boolean isSizeIncreasing = (repositionedHeight - startingHeight) > 0;
@@ -284,12 +304,19 @@
&& DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS.isTrue();
}
- interface DragStartListener {
+ public interface DragEventListener {
/**
* Inform the implementing class that a drag resize has started
*
* @param taskId id of this positioner's {@link WindowDecoration}
*/
void onDragStart(int taskId);
+
+ /**
+ * Inform the implementing class that a drag move has started.
+ *
+ * @param taskId id of this positioner's {@link WindowDecoration}
+ */
+ void onDragMove(int taskId);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index 844ceb3..6f72d34 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -42,7 +42,7 @@
/**
* Geometry for a drag resize region for a particular window.
*/
-final class DragResizeWindowGeometry {
+public final class DragResizeWindowGeometry {
private final int mTaskCornerRadius;
private final Size mTaskSize;
// The size of the handle outside the task window applied to the edges of the window, for the
@@ -58,19 +58,24 @@
// The bounds for each edge drag region, which can resize the task in one direction.
final @NonNull TaskEdges mTaskEdges;
+ private final DisabledEdge mDisabledEdge;
+
DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize,
int resizeHandleEdgeOutset, int resizeHandleEdgeInset, int fineCornerSize,
- int largeCornerSize) {
+ int largeCornerSize, DisabledEdge disabledEdge) {
mTaskCornerRadius = taskCornerRadius;
mTaskSize = taskSize;
mResizeHandleEdgeOutset = resizeHandleEdgeOutset;
mResizeHandleEdgeInset = resizeHandleEdgeInset;
- mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize);
- mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize);
+ mDisabledEdge = disabledEdge;
+
+ mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize, disabledEdge);
+ mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize, disabledEdge);
// Save touch areas for each edge.
- mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset);
+ mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset,
+ mDisabledEdge);
}
/**
@@ -170,7 +175,7 @@
|| e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE
// Touchpad input
|| (e.isFromSource(SOURCE_MOUSE)
- && e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER);
+ && e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER);
} else {
return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
}
@@ -187,8 +192,9 @@
/**
* Returns the control type for the drag-resize, based on the touch regions and this
* MotionEvent's coordinates.
+ *
* @param isTouchscreen Controls the size of the corner resize regions; touchscreen events
- * (finger & stylus) are eligible for a larger area than cursor events
+ * (finger & stylus) are eligible for a larger area than cursor events.
* @param isEdgeResizePermitted Indicates if the event is eligible for falling into an edge
* resize region.
*/
@@ -252,6 +258,10 @@
@DragPositioningCallback.CtrlType
private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, float x,
float y) {
+ if ((mDisabledEdge == DisabledEdge.RIGHT && (ctrlType & CTRL_TYPE_RIGHT) != 0)
+ || mDisabledEdge == DisabledEdge.LEFT && ((ctrlType & CTRL_TYPE_LEFT) != 0)) {
+ return CTRL_TYPE_UNDEFINED;
+ }
final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType);
double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y);
@@ -337,29 +347,31 @@
private final @NonNull Rect mRightTopCornerBounds;
private final @NonNull Rect mLeftBottomCornerBounds;
private final @NonNull Rect mRightBottomCornerBounds;
+ private final @NonNull DisabledEdge mDisabledEdge;
- TaskCorners(@NonNull Size taskSize, int cornerSize) {
+ TaskCorners(@NonNull Size taskSize, int cornerSize, DisabledEdge disabledEdge) {
mCornerSize = cornerSize;
+ mDisabledEdge = disabledEdge;
final int cornerRadius = cornerSize / 2;
- mLeftTopCornerBounds = new Rect(
+ mLeftTopCornerBounds = (disabledEdge == DisabledEdge.LEFT) ? new Rect() : new Rect(
-cornerRadius,
-cornerRadius,
cornerRadius,
cornerRadius);
- mRightTopCornerBounds = new Rect(
+ mRightTopCornerBounds = (disabledEdge == DisabledEdge.RIGHT) ? new Rect() : new Rect(
taskSize.getWidth() - cornerRadius,
-cornerRadius,
taskSize.getWidth() + cornerRadius,
cornerRadius);
- mLeftBottomCornerBounds = new Rect(
+ mLeftBottomCornerBounds = (disabledEdge == DisabledEdge.LEFT) ? new Rect() : new Rect(
-cornerRadius,
taskSize.getHeight() - cornerRadius,
cornerRadius,
taskSize.getHeight() + cornerRadius);
- mRightBottomCornerBounds = new Rect(
+ mRightBottomCornerBounds = (disabledEdge == DisabledEdge.RIGHT) ? new Rect() : new Rect(
taskSize.getWidth() - cornerRadius,
taskSize.getHeight() - cornerRadius,
taskSize.getWidth() + cornerRadius,
@@ -370,10 +382,14 @@
* Updates the region to include all four corners.
*/
void union(Region region) {
- region.union(mLeftTopCornerBounds);
- region.union(mRightTopCornerBounds);
- region.union(mLeftBottomCornerBounds);
- region.union(mRightBottomCornerBounds);
+ if (mDisabledEdge != DisabledEdge.RIGHT) {
+ region.union(mRightTopCornerBounds);
+ region.union(mRightBottomCornerBounds);
+ }
+ if (mDisabledEdge != DisabledEdge.LEFT) {
+ region.union(mLeftTopCornerBounds);
+ region.union(mLeftBottomCornerBounds);
+ }
}
/**
@@ -440,9 +456,12 @@
private final @NonNull Rect mRightEdgeBounds;
private final @NonNull Rect mBottomEdgeBounds;
private final @NonNull Region mRegion;
+ private final @NonNull DisabledEdge mDisabledEdge;
private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness,
- int resizeHandleEdgeInset) {
+ int resizeHandleEdgeInset, DisabledEdge disabledEdge) {
+ // Save touch areas for each edge.
+ mDisabledEdge = disabledEdge;
// Save touch areas for each edge.
mTopEdgeBounds = new Rect(
-resizeHandleThickness,
@@ -466,10 +485,7 @@
taskSize.getHeight() + resizeHandleThickness);
mRegion = new Region();
- mRegion.union(mTopEdgeBounds);
- mRegion.union(mLeftEdgeBounds);
- mRegion.union(mRightEdgeBounds);
- mRegion.union(mBottomEdgeBounds);
+ union(mRegion);
}
/**
@@ -483,9 +499,13 @@
* Updates the region to include all four corners.
*/
private void union(Region region) {
+ if (mDisabledEdge != DisabledEdge.RIGHT) {
+ region.union(mRightEdgeBounds);
+ }
+ if (mDisabledEdge != DisabledEdge.LEFT) {
+ region.union(mLeftEdgeBounds);
+ }
region.union(mTopEdgeBounds);
- region.union(mLeftEdgeBounds);
- region.union(mRightEdgeBounds);
region.union(mBottomEdgeBounds);
}
@@ -519,4 +539,10 @@
mBottomEdgeBounds);
}
}
+
+ public enum DisabledEdge {
+ LEFT,
+ RIGHT,
+ NONE
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index ccf329c..3efae9d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -35,6 +35,7 @@
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.transition.Transitions;
+import java.util.ArrayList;
import java.util.function.Supplier;
/**
@@ -55,7 +56,8 @@
private final WindowDecoration mWindowDecoration;
private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
private DisplayController mDisplayController;
- private DragPositioningCallbackUtility.DragStartListener mDragStartListener;
+ private ArrayList<DragPositioningCallbackUtility.DragEventListener> mDragEventListeners =
+ new ArrayList<>();
private final Rect mStableBounds = new Rect();
private final Rect mTaskBoundsAtDragStart = new Rect();
private final PointF mRepositionStartPoint = new PointF();
@@ -69,20 +71,22 @@
FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, Transitions transitions,
WindowDecoration windowDecoration, DisplayController displayController) {
this(taskOrganizer, transitions, windowDecoration, displayController,
- dragStartListener -> {}, SurfaceControl.Transaction::new);
+ null, SurfaceControl.Transaction::new);
}
FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
Transitions transitions,
WindowDecoration windowDecoration,
DisplayController displayController,
- DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ DragPositioningCallbackUtility.DragEventListener dragEventListener,
Supplier<SurfaceControl.Transaction> supplier) {
mTaskOrganizer = taskOrganizer;
mTransitions = transitions;
mWindowDecoration = windowDecoration;
mDisplayController = displayController;
- mDragStartListener = dragStartListener;
+ if (dragEventListener != null) {
+ mDragEventListeners.add(dragEventListener);
+ }
mTransactionSupplier = supplier;
}
@@ -92,7 +96,9 @@
mTaskBoundsAtDragStart.set(
mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
mRepositionStartPoint.set(x, y);
- mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
+ for (DragPositioningCallbackUtility.DragEventListener listener : mDragEventListeners) {
+ listener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
+ }
if (mCtrlType != CTRL_TYPE_UNDEFINED && !mWindowDecoration.mHasGlobalFocus) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mWindowDecoration.mTaskInfo.token, true /* onTop */,
@@ -120,6 +126,10 @@
// The task is being resized, send the |dragResizing| hint to core with the first
// bounds-change wct.
if (!mHasDragResized) {
+ for (DragPositioningCallbackUtility.DragEventListener listener :
+ mDragEventListeners) {
+ listener.onDragMove(mWindowDecoration.mTaskInfo.taskId);
+ }
// This is the first bounds change since drag resize operation started.
wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
}
@@ -216,4 +226,16 @@
public boolean isResizingOrAnimating() {
return mIsResizingOrAnimatingResize;
}
+
+ @Override
+ public void addDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+ mDragEventListeners.add(dragEventListener);
+ }
+
+ @Override
+ public void removeDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+ mDragEventListeners.remove(dragEventListener);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index fb81ed4..8770d35 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -49,7 +49,7 @@
/**
* Creates and updates a veil that covers task contents on resize.
*/
-class ResizeVeil @JvmOverloads constructor(
+public class ResizeVeil @JvmOverloads constructor(
private val context: Context,
private val displayController: DisplayController,
private val appIcon: Bitmap,
@@ -188,28 +188,16 @@
t.apply()
return
}
- isVisible = true
val background = backgroundSurface
val icon = iconSurface
- val veil = veilSurface
- if (background == null || icon == null || veil == null) return
-
- // Parent surface can change, ensure it is up to date.
- if (parent != parentSurface) {
- t.reparent(veil, parent)
- parentSurface = parent
- }
-
- val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) {
- Theme.LIGHT -> lightColors.surfaceContainer
- Theme.DARK -> darkColors.surfaceContainer
- }
- t.show(veil)
- .setLayer(veil, VEIL_CONTAINER_LAYER)
- .setLayer(icon, VEIL_ICON_LAYER)
- .setLayer(background, VEIL_BACKGROUND_LAYER)
- .setColor(background, Color.valueOf(backgroundColor.toArgb()).components)
- relayout(taskBounds, t)
+ if (background == null || icon == null) return
+ updateTransactionWithShowVeil(
+ t,
+ parent,
+ taskBounds,
+ taskInfo,
+ fadeIn,
+ )
if (fadeIn) {
cancelAnimation()
val veilAnimT = surfaceControlTransactionSupplier.get()
@@ -259,11 +247,43 @@
iconAnimator.start()
} else {
// Show the veil immediately.
+ t.apply()
+ }
+ }
+
+ fun updateTransactionWithShowVeil(
+ t: SurfaceControl.Transaction,
+ parent: SurfaceControl,
+ taskBounds: Rect,
+ taskInfo: RunningTaskInfo,
+ fadeIn: Boolean = false,
+ ) {
+ if (!isReady || isVisible) return
+ isVisible = true
+ val background = backgroundSurface
+ val icon = iconSurface
+ val veil = veilSurface
+ if (background == null || icon == null || veil == null) return
+ // Parent surface can change, ensure it is up to date.
+ if (parent != parentSurface) {
+ t.reparent(veil, parent)
+ parentSurface = parent
+ }
+ val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) {
+ Theme.LIGHT -> lightColors.surfaceContainer
+ Theme.DARK -> darkColors.surfaceContainer
+ }
+ t.show(veil)
+ .setLayer(veil, VEIL_CONTAINER_LAYER)
+ .setLayer(icon, VEIL_ICON_LAYER)
+ .setLayer(background, VEIL_BACKGROUND_LAYER)
+ .setColor(background, Color.valueOf(backgroundColor.toArgb()).components)
+ relayout(taskBounds, t)
+ if (!fadeIn) {
t.show(icon)
- .show(background)
- .setAlpha(icon, 1f)
- .setAlpha(background, 1f)
- .apply()
+ .show(background)
+ .setAlpha(icon, 1f)
+ .setAlpha(background, 1f)
}
}
@@ -314,8 +334,12 @@
* @param newBounds bounds to update veil to.
*/
fun updateResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) {
+ updateTransactionWithResizeVeil(t, newBounds)
+ t.apply()
+ }
+
+ fun updateTransactionWithResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) {
if (!isVisible) {
- t.apply()
return
}
veilAnimator?.let { animator ->
@@ -325,7 +349,6 @@
}
}
relayout(newBounds, t)
- t.apply()
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
index d7ea0c3..63b288d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskDragResizer.java
@@ -26,4 +26,19 @@
* a resize is complete.
*/
boolean isResizingOrAnimating();
+
+ /**
+ * Adds a drag start listener to be notified of drag start events.
+ *
+ * @param dragEventListener Listener to be added.
+ */
+ void addDragEventListener(DragPositioningCallbackUtility.DragEventListener dragEventListener);
+
+ /**
+ * Removes a drag start listener from the listener set.
+ *
+ * @param dragEventListener Listener to be removed.
+ */
+ void removeDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index ff3b455..a1e329a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -43,6 +43,7 @@
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.transition.Transitions;
+import java.util.ArrayList;
import java.util.function.Supplier;
/**
@@ -56,7 +57,8 @@
private DesktopModeWindowDecoration mDesktopWindowDecoration;
private ShellTaskOrganizer mTaskOrganizer;
private DisplayController mDisplayController;
- private DragPositioningCallbackUtility.DragStartListener mDragStartListener;
+ private ArrayList<DragPositioningCallbackUtility.DragEventListener>
+ mDragEventListeners = new ArrayList<>();
private final Transitions mTransitions;
private final Rect mStableBounds = new Rect();
private final Rect mTaskBoundsAtDragStart = new Rect();
@@ -73,23 +75,23 @@
public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
DesktopModeWindowDecoration windowDecoration,
DisplayController displayController,
- DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ DragPositioningCallbackUtility.DragEventListener dragEventListener,
Transitions transitions, InteractionJankMonitor interactionJankMonitor,
@ShellMainThread Handler handler) {
- this(taskOrganizer, windowDecoration, displayController, dragStartListener,
+ this(taskOrganizer, windowDecoration, displayController, dragEventListener,
SurfaceControl.Transaction::new, transitions, interactionJankMonitor, handler);
}
public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
DesktopModeWindowDecoration windowDecoration,
DisplayController displayController,
- DragPositioningCallbackUtility.DragStartListener dragStartListener,
+ DragPositioningCallbackUtility.DragEventListener dragEventListener,
Supplier<SurfaceControl.Transaction> supplier, Transitions transitions,
InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler) {
mDesktopWindowDecoration = windowDecoration;
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
- mDragStartListener = dragStartListener;
+ mDragEventListeners.add(dragEventListener);
mTransactionSupplier = supplier;
mTransitions = transitions;
mInteractionJankMonitor = interactionJankMonitor;
@@ -113,7 +115,10 @@
mTaskOrganizer.applyTransaction(wct);
}
}
- mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
+ for (DragPositioningCallbackUtility.DragEventListener dragEventListener :
+ mDragEventListeners) {
+ dragEventListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
+ }
mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
int rotation = mDesktopWindowDecoration
.mTaskInfo.configuration.windowConfiguration.getDisplayRotation();
@@ -137,6 +142,10 @@
mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta,
mDisplayController, mDesktopWindowDecoration)) {
if (!mIsResizingOrAnimatingResize) {
+ for (DragPositioningCallbackUtility.DragEventListener dragEventListener :
+ mDragEventListeners) {
+ dragEventListener.onDragMove(mDesktopWindowDecoration.mTaskInfo.taskId);
+ }
mDesktopWindowDecoration.showResizeVeil(mRepositionTaskBounds);
mIsResizingOrAnimatingResize = true;
} else {
@@ -237,4 +246,16 @@
public boolean isResizingOrAnimating() {
return mIsResizingOrAnimatingResize;
}
+
+ @Override
+ public void addDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+ mDragEventListeners.add(dragEventListener);
+ }
+
+ @Override
+ public void removeDragEventListener(
+ DragPositioningCallbackUtility.DragEventListener dragEventListener) {
+ mDragEventListeners.remove(dragEventListener);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 34cc098..f97dfb89 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -131,12 +131,15 @@
}
};
- RunningTaskInfo mTaskInfo;
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public RunningTaskInfo mTaskInfo;
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public Context mDecorWindowContext;
int mLayoutResId;
- final SurfaceControl mTaskSurface;
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public final SurfaceControl mTaskSurface;
Display mDisplay;
- Context mDecorWindowContext;
SurfaceControl mDecorationContainerSurface;
SurfaceControl mCaptionContainerSurface;
@@ -200,6 +203,14 @@
}
/**
+ * Gets the decoration's task leash.
+ * @return the decoration' task surface used to manipulate the task.
+ */
+ public SurfaceControl getLeash() {
+ return mTaskSurface;
+ }
+
+ /**
* Used by {@link WindowDecoration} to trigger a new relayout because the requirements for a
* relayout weren't satisfied are satisfied now.
*
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
new file mode 100644
index 0000000..ff418c6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.graphics.Rect
+import android.util.SparseArray
+import androidx.core.util.valueIterator
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+
+/** Manages tiling for each displayId/userId independently. */
+class DesktopTilingDecorViewModel(
+ private val context: Context,
+ private val displayController: DisplayController,
+ private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val syncQueue: SyncTransactionQueue,
+ private val transitions: Transitions,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
+ private val returnToDragStartAnimator: ReturnToDragStartAnimator,
+ private val taskRepository: DesktopRepository,
+) {
+ @VisibleForTesting
+ var tilingTransitionHandlerByDisplayId = SparseArray<DesktopTilingWindowDecoration>()
+
+ fun snapToHalfScreen(
+ taskInfo: ActivityManager.RunningTaskInfo,
+ desktopModeWindowDecoration: DesktopModeWindowDecoration,
+ position: DesktopTasksController.SnapPosition,
+ destinationBounds: Rect,
+ ): Boolean {
+ val displayId = taskInfo.displayId
+ val handler =
+ tilingTransitionHandlerByDisplayId.get(displayId)
+ ?: run {
+ val newHandler =
+ DesktopTilingWindowDecoration(
+ context,
+ syncQueue,
+ displayController,
+ displayId,
+ rootTdaOrganizer,
+ transitions,
+ shellTaskOrganizer,
+ toggleResizeDesktopTaskTransitionHandler,
+ returnToDragStartAnimator,
+ taskRepository,
+ )
+ tilingTransitionHandlerByDisplayId.put(displayId, newHandler)
+ newHandler
+ }
+ transitions.registerObserver(handler)
+ return handler.onAppTiled(
+ taskInfo,
+ desktopModeWindowDecoration,
+ position,
+ destinationBounds,
+ )
+ }
+
+ fun removeTaskIfTiled(displayId: Int, taskId: Int) {
+ tilingTransitionHandlerByDisplayId.get(displayId)?.removeTaskIfTiled(taskId)
+ }
+
+ fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean {
+ return tilingTransitionHandlerByDisplayId
+ .get(taskInfo.displayId)
+ ?.moveTiledPairToFront(taskInfo) ?: false
+ }
+
+ fun onOverviewAnimationStateChange(isRunning: Boolean) {
+ for (tilingHandler in tilingTransitionHandlerByDisplayId.valueIterator()) {
+ tilingHandler.onOverviewAnimationStateChange(isRunning)
+ }
+ }
+
+ fun onUserChange() {
+ for (tilingHandler in tilingTransitionHandlerByDisplayId.valueIterator()) {
+ tilingHandler.onUserChange()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
new file mode 100644
index 0000000..9bf1304
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.graphics.Region
+import android.os.Binder
+import android.view.LayoutInflater
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+import android.view.WindowManager.LayoutParams.FLAG_SLIPPERY
+import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER
+import android.view.WindowlessWindowManager
+import com.android.wm.shell.R
+import com.android.wm.shell.common.SyncTransactionQueue
+import java.util.function.Supplier
+
+/**
+ * a [WindowlessWindowManaer] responsible for hosting the [TilingDividerView] on the display root
+ * when two tasks are tiled on left and right to resize them simultaneously.
+ */
+class DesktopTilingDividerWindowManager(
+ private val config: Configuration,
+ private val windowName: String,
+ private val context: Context,
+ private val leash: SurfaceControl,
+ private val syncQueue: SyncTransactionQueue,
+ private val transitionHandler: DesktopTilingWindowDecoration,
+ private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
+ private var dividerBounds: Rect,
+) : WindowlessWindowManager(config, leash, null), DividerMoveCallback, View.OnLayoutChangeListener {
+ private lateinit var viewHost: SurfaceControlViewHost
+ private var tilingDividerView: TilingDividerView? = null
+ private var dividerShown = false
+ private var handleRegionWidth: Int = -1
+ private var setTouchRegion = true
+
+ /**
+ * Gets bounds of divider window with screen based coordinate on the param Rect.
+ *
+ * @param rect bounds for the [TilingDividerView]
+ */
+ fun getDividerBounds(rect: Rect) {
+ rect.set(dividerBounds)
+ }
+
+ /** Sets the touch region for the SurfaceControlViewHost. */
+ fun setTouchRegion(region: Rect) {
+ setTouchRegion(viewHost.windowToken.asBinder(), Region(region))
+ }
+
+ /**
+ * Builds a view host upon tiling two tasks left and right, and shows the divider view in the
+ * middle of the screen between both tasks.
+ *
+ * @param relativeLeash the task leash that the TilingDividerView should be shown on top of.
+ */
+ fun generateViewHost(relativeLeash: SurfaceControl) {
+ val t = transactionSupplier.get()
+ val surfaceControlViewHost =
+ SurfaceControlViewHost(context, context.display, this, "DesktopTilingManager")
+ val dividerView =
+ LayoutInflater.from(context).inflate(R.layout.tiling_split_divider, /* root= */ null)
+ as TilingDividerView
+ val lp = getWindowManagerParams()
+ surfaceControlViewHost.setView(dividerView, lp)
+ val tmpDividerBounds = Rect()
+ getDividerBounds(tmpDividerBounds)
+ dividerView.setup(this, tmpDividerBounds)
+ t.setRelativeLayer(leash, relativeLeash, 1)
+ .setPosition(leash, dividerBounds.left.toFloat(), dividerBounds.top.toFloat())
+ .show(leash)
+ syncQueue.runInSync { transaction ->
+ transaction.merge(t)
+ t.close()
+ }
+ dividerShown = true
+ viewHost = surfaceControlViewHost
+ dividerView.addOnLayoutChangeListener(this)
+ tilingDividerView = dividerView
+ handleRegionWidth = dividerView.handleRegionWidth
+ }
+
+ /** Hides the divider bar. */
+ fun hideDividerBar() {
+ if (!dividerShown) {
+ return
+ }
+ val t = transactionSupplier.get()
+ t.hide(leash)
+ t.apply()
+ dividerShown = false
+ }
+
+ /** Shows the divider bar. */
+ fun showDividerBar() {
+ if (dividerShown) return
+ val t = transactionSupplier.get()
+ t.show(leash)
+ t.apply()
+ dividerShown = true
+ }
+
+ /**
+ * When the tiled task on top changes, the divider bar's Z access should change to be on top of
+ * the latest focused task.
+ */
+ fun onRelativeLeashChanged(relativeLeash: SurfaceControl, t: SurfaceControl.Transaction) {
+ t.setRelativeLayer(leash, relativeLeash, 1)
+ }
+
+ override fun onDividerMoveStart(pos: Int) {
+ setSlippery(false)
+ }
+
+ /**
+ * Moves the divider view to a new position after touch, gets called from the
+ * [TilingDividerView] onTouch function.
+ */
+ override fun onDividerMove(pos: Int): Boolean {
+ val t = transactionSupplier.get()
+ t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat())
+ val dividerWidth = dividerBounds.width()
+ dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom)
+ return transitionHandler.onDividerHandleMoved(dividerBounds, t)
+ }
+
+ /**
+ * Notifies the transition handler of tiling operations ending, which might result in resizing
+ * WindowContainerTransactions if the sizes of the tiled tasks changed.
+ */
+ override fun onDividerMovedEnd(pos: Int) {
+ setSlippery(true)
+ val t = transactionSupplier.get()
+ t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat())
+ val dividerWidth = dividerBounds.width()
+ dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom)
+ transitionHandler.onDividerHandleDragEnd(dividerBounds, t)
+ }
+
+ private fun getWindowManagerParams(): WindowManager.LayoutParams {
+ val lp =
+ WindowManager.LayoutParams(
+ dividerBounds.width(),
+ dividerBounds.height(),
+ TYPE_DOCK_DIVIDER,
+ FLAG_NOT_FOCUSABLE or
+ FLAG_NOT_TOUCH_MODAL or
+ FLAG_WATCH_OUTSIDE_TOUCH or
+ FLAG_SPLIT_TOUCH or
+ FLAG_SLIPPERY,
+ PixelFormat.TRANSLUCENT,
+ )
+ lp.token = Binder()
+ lp.title = windowName
+ lp.privateFlags =
+ lp.privateFlags or (PRIVATE_FLAG_NO_MOVE_ANIMATION or PRIVATE_FLAG_TRUSTED_OVERLAY)
+ return lp
+ }
+
+ /**
+ * Releases the surface control of the current [TilingDividerView] and tear down the view
+ * hierarchy.y.
+ */
+ fun release() {
+ tilingDividerView = null
+ viewHost.release()
+ transactionSupplier.get().hide(leash).remove(leash).apply()
+ }
+
+ override fun onLayoutChange(
+ v: View?,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int,
+ ) {
+ if (!setTouchRegion) return
+
+ val startX = (dividerBounds.width() - handleRegionWidth) / 2
+ val startY = 0
+ val tempRect = Rect(startX, startY, startX + handleRegionWidth, dividerBounds.height())
+ setTouchRegion(tempRect)
+ setTouchRegion = false
+ }
+
+ private fun setSlippery(slippery: Boolean) {
+ val lp = tilingDividerView?.layoutParams as WindowManager.LayoutParams
+ val isSlippery = (lp.flags and FLAG_SLIPPERY) != 0
+ if (isSlippery == slippery) return
+
+ if (slippery) {
+ lp.flags = lp.flags or FLAG_SLIPPERY
+ } else {
+ lp.flags = lp.flags and FLAG_SLIPPERY.inv()
+ }
+ viewHost.relayout(lp)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
new file mode 100644
index 0000000..6ea1d14
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt
@@ -0,0 +1,654 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.os.IBinder
+import android.util.Slog
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.internal.annotations.VisibleForTesting
+import com.android.launcher3.icons.BaseIconFactory
+import com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT
+import com.android.launcher3.icons.IconProvider
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TRANSIT_MINIMIZE
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
+import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE
+import com.android.wm.shell.windowdecor.ResizeVeil
+import com.android.wm.shell.windowdecor.extension.isFullscreen
+import java.util.function.Supplier
+
+class DesktopTilingWindowDecoration(
+ private var context: Context,
+ private val syncQueue: SyncTransactionQueue,
+ private val displayController: DisplayController,
+ private val displayId: Int,
+ private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val transitions: Transitions,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
+ private val returnToDragStartAnimator: ReturnToDragStartAnimator,
+ private val taskRepository: DesktopRepository,
+ private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
+) :
+ Transitions.TransitionHandler,
+ ShellTaskOrganizer.FocusListener,
+ ShellTaskOrganizer.TaskVanishedListener,
+ DragEventListener,
+ Transitions.TransitionObserver {
+ companion object {
+ private val TAG: String = DesktopTilingWindowDecoration::class.java.simpleName
+ private const val TILING_DIVIDER_TAG = "Tiling Divider"
+ }
+
+ var leftTaskResizingHelper: AppResizingHelper? = null
+ var rightTaskResizingHelper: AppResizingHelper? = null
+ private var isTilingManagerInitialised = false
+ @VisibleForTesting
+ var desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager? = null
+ private lateinit var dividerBounds: Rect
+ private var isResizing = false
+ private var isTilingFocused = false
+
+ fun onAppTiled(
+ taskInfo: RunningTaskInfo,
+ desktopModeWindowDecoration: DesktopModeWindowDecoration,
+ position: SnapPosition,
+ currentBounds: Rect,
+ ): Boolean {
+ val destinationBounds = getSnapBounds(taskInfo, position)
+ val resizeMetadata =
+ AppResizingHelper(
+ taskInfo,
+ desktopModeWindowDecoration,
+ context,
+ destinationBounds,
+ displayController,
+ transactionSupplier,
+ )
+ val isFirstTiledApp = leftTaskResizingHelper == null && rightTaskResizingHelper == null
+ val isTiled = destinationBounds != taskInfo.configuration.windowConfiguration.bounds
+
+ initTilingApps(resizeMetadata, position, taskInfo)
+ // Observe drag resizing to break tiling if a task is drag resized.
+ desktopModeWindowDecoration.addDragResizeListener(this)
+
+ if (isTiled) {
+ val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
+ toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentBounds)
+ } else {
+ // Handle the case where we attempt to snap resize when already snap resized: the task
+ // position won't need to change but we want to animate the surface going back to the
+ // snapped position from the "dragged-to-the-edge" position.
+ if (destinationBounds != currentBounds) {
+ returnToDragStartAnimator.start(
+ taskInfo.taskId,
+ resizeMetadata.getLeash(),
+ startBounds = currentBounds,
+ endBounds = destinationBounds,
+ isResizable = taskInfo.isResizeable,
+ )
+ }
+ }
+ initTilingForDisplayIfNeeded(taskInfo.configuration, isFirstTiledApp)
+ return isTiled
+ }
+
+ // If a task is already tiled on the same position, release this task, otherwise if the same
+ // task is tiled on the opposite side, remove it from the opposite side so it's tiled correctly.
+ private fun initTilingApps(
+ taskResizingHelper: AppResizingHelper,
+ position: SnapPosition,
+ taskInfo: RunningTaskInfo,
+ ) {
+ when (position) {
+ SnapPosition.RIGHT -> {
+ rightTaskResizingHelper?.let { removeTaskIfTiled(it.taskInfo.taskId) }
+ if (leftTaskResizingHelper?.taskInfo?.taskId == taskInfo.taskId) {
+ removeTaskIfTiled(taskInfo.taskId)
+ }
+ rightTaskResizingHelper = taskResizingHelper
+ }
+
+ SnapPosition.LEFT -> {
+ leftTaskResizingHelper?.let { removeTaskIfTiled(it.taskInfo.taskId) }
+ if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+ removeTaskIfTiled(taskInfo.taskId)
+ }
+ leftTaskResizingHelper = taskResizingHelper
+ }
+ }
+ }
+
+ private fun initTilingForDisplayIfNeeded(config: Configuration, firstTiledApp: Boolean) {
+ if (leftTaskResizingHelper != null && rightTaskResizingHelper != null) {
+ if (!isTilingManagerInitialised) {
+ desktopTilingDividerWindowManager = initTilingManagerForDisplay(displayId, config)
+ isTilingManagerInitialised = true
+ shellTaskOrganizer.addFocusListener(this)
+ isTilingFocused = true
+ }
+ leftTaskResizingHelper?.initIfNeeded()
+ rightTaskResizingHelper?.initIfNeeded()
+ leftTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(
+ DragResizeWindowGeometry.DisabledEdge.RIGHT,
+ /* shouldDelayUpdate = */ false,
+ )
+ rightTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(
+ DragResizeWindowGeometry.DisabledEdge.LEFT,
+ /* shouldDelayUpdate = */ false,
+ )
+ } else if (firstTiledApp) {
+ shellTaskOrganizer.addTaskVanishedListener(this)
+ }
+ }
+
+ private fun initTilingManagerForDisplay(
+ displayId: Int,
+ config: Configuration,
+ ): DesktopTilingDividerWindowManager? {
+ val displayLayout = displayController.getDisplayLayout(displayId)
+ val builder = SurfaceControl.Builder()
+ rootTdaOrganizer.attachToDisplayArea(displayId, builder)
+ val leash = builder.setName(TILING_DIVIDER_TAG).setContainerLayer().build()
+ val tilingManager =
+ displayLayout?.let {
+ dividerBounds = inflateDividerBounds(it)
+ DesktopTilingDividerWindowManager(
+ config,
+ TAG,
+ context,
+ leash,
+ syncQueue,
+ this,
+ transactionSupplier,
+ dividerBounds,
+ )
+ }
+ // a leash to present the divider on top of, without re-parenting.
+ val relativeLeash =
+ leftTaskResizingHelper?.desktopModeWindowDecoration?.getLeash() ?: return tilingManager
+ tilingManager?.generateViewHost(relativeLeash)
+ return tilingManager
+ }
+
+ fun onDividerHandleMoved(dividerBounds: Rect, t: SurfaceControl.Transaction): Boolean {
+ val leftTiledTask = leftTaskResizingHelper ?: return false
+ val rightTiledTask = rightTaskResizingHelper ?: return false
+ val stableBounds = Rect()
+ val displayLayout = displayController.getDisplayLayout(displayId)
+ displayLayout?.getStableBounds(stableBounds)
+
+ if (stableBounds.isEmpty) return false
+
+ val leftBounds = leftTiledTask.bounds
+ val rightBounds = rightTiledTask.bounds
+ val newLeftBounds =
+ Rect(leftBounds.left, leftBounds.top, dividerBounds.left, leftBounds.bottom)
+ val newRightBounds =
+ Rect(dividerBounds.right, rightBounds.top, rightBounds.right, rightBounds.bottom)
+
+ // If one of the apps is getting smaller or bigger than size constraint, ignore finger move.
+ if (
+ isResizeWithinSizeConstraints(
+ newLeftBounds,
+ newRightBounds,
+ leftBounds,
+ rightBounds,
+ stableBounds,
+ )
+ ) {
+ return false
+ }
+
+ // The final new bounds for each app has to be registered to make sure a startAnimate
+ // when the new bounds are different from old bounds, otherwise hide the veil without
+ // waiting for an animation as no animation will run when no bounds are changed.
+ leftTiledTask.newBounds.set(newLeftBounds)
+ rightTiledTask.newBounds.set(newRightBounds)
+ if (!isResizing) {
+ leftTiledTask.showVeil(t)
+ rightTiledTask.showVeil(t)
+ isResizing = true
+ } else {
+ leftTiledTask.updateVeil(t)
+ rightTiledTask.updateVeil(t)
+ }
+
+ // Applies showing/updating veil for both apps and moving the divider into its new position.
+ t.apply()
+ return true
+ }
+
+ fun onDividerHandleDragEnd(dividerBounds: Rect, t: SurfaceControl.Transaction) {
+ val leftTiledTask = leftTaskResizingHelper ?: return
+ val rightTiledTask = rightTaskResizingHelper ?: return
+
+ if (leftTiledTask.newBounds == leftTiledTask.bounds) {
+ leftTiledTask.hideVeil()
+ rightTiledTask.hideVeil()
+ isResizing = false
+ return
+ }
+ leftTiledTask.bounds.set(leftTiledTask.newBounds)
+ rightTiledTask.bounds.set(rightTiledTask.newBounds)
+ onDividerHandleMoved(dividerBounds, t)
+ isResizing = false
+ val wct = WindowContainerTransaction()
+ wct.setBounds(leftTiledTask.taskInfo.token, leftTiledTask.bounds)
+ wct.setBounds(rightTiledTask.taskInfo.token, rightTiledTask.bounds)
+ transitions.startTransition(TRANSIT_CHANGE, wct, this)
+ }
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ val leftTiledTask = leftTaskResizingHelper ?: return false
+ val rightTiledTask = rightTaskResizingHelper ?: return false
+ for (change in info.getChanges()) {
+ val sc: SurfaceControl = change.getLeash()
+ val endBounds =
+ if (change.taskInfo?.taskId == leftTiledTask.taskInfo.taskId) {
+ leftTiledTask.bounds
+ } else {
+ rightTiledTask.bounds
+ }
+ startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+ finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
+ }
+
+ startTransaction.apply()
+ leftTiledTask.hideVeil()
+ rightTiledTask.hideVeil()
+ finishCallback.onTransitionFinished(null)
+ return true
+ }
+
+ // TODO(b/361505243) bring tasks to front here when the empty request info bug is fixed.
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? {
+ return null
+ }
+
+ override fun onDragStart(taskId: Int) {}
+
+ override fun onDragMove(taskId: Int) {
+ removeTaskIfTiled(taskId)
+ }
+
+ override fun onTransitionReady(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ ) {
+ for (change in info.changes) {
+ change.taskInfo?.let {
+ if (it.isFullscreen || isMinimized(change.mode, info.type)) {
+ removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen)
+ }
+ }
+ }
+ }
+
+ private fun isMinimized(changeMode: Int, infoType: Int): Boolean {
+ return (changeMode == TRANSIT_TO_BACK &&
+ (infoType == TRANSIT_MINIMIZE || infoType == TRANSIT_TO_BACK))
+ }
+
+ class AppResizingHelper(
+ val taskInfo: RunningTaskInfo,
+ val desktopModeWindowDecoration: DesktopModeWindowDecoration,
+ val context: Context,
+ val bounds: Rect,
+ val displayController: DisplayController,
+ val transactionSupplier: Supplier<Transaction>,
+ ) {
+ var isInitialised = false
+ var newBounds = Rect(bounds)
+ private lateinit var resizeVeilBitmap: Bitmap
+ private lateinit var resizeVeil: ResizeVeil
+ private val displayContext = displayController.getDisplayContext(taskInfo.displayId)
+
+ fun initIfNeeded() {
+ if (!isInitialised) {
+ initVeil()
+ isInitialised = true
+ }
+ }
+
+ private fun initVeil() {
+ val baseActivity = taskInfo.baseActivity
+ if (baseActivity == null) {
+ Slog.e(TAG, "Base activity component not found in task")
+ return
+ }
+ val resizeVeilIconFactory =
+ displayContext?.let {
+ createIconFactory(displayContext, R.dimen.desktop_mode_resize_veil_icon_size)
+ } ?: return
+ val pm = context.getApplicationContext().getPackageManager()
+ val activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */)
+ val provider = IconProvider(displayContext)
+ val appIconDrawable = provider.getIcon(activityInfo)
+ resizeVeilBitmap =
+ resizeVeilIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT)
+ resizeVeil =
+ ResizeVeil(
+ context = displayContext,
+ displayController = displayController,
+ appIcon = resizeVeilBitmap,
+ parentSurface = desktopModeWindowDecoration.getLeash(),
+ surfaceControlTransactionSupplier = transactionSupplier,
+ taskInfo = taskInfo,
+ )
+ }
+
+ fun showVeil(t: Transaction) =
+ resizeVeil.updateTransactionWithShowVeil(
+ t,
+ desktopModeWindowDecoration.getLeash(),
+ bounds,
+ taskInfo,
+ )
+
+ fun updateVeil(t: Transaction) = resizeVeil.updateTransactionWithResizeVeil(t, newBounds)
+
+ fun hideVeil() = resizeVeil.hideVeil()
+
+ private fun createIconFactory(context: Context, dimensions: Int): BaseIconFactory {
+ val resources: Resources = context.resources
+ val densityDpi: Int = resources.getDisplayMetrics().densityDpi
+ val iconSize: Int = resources.getDimensionPixelSize(dimensions)
+ return BaseIconFactory(context, densityDpi, iconSize)
+ }
+
+ fun getLeash(): SurfaceControl = desktopModeWindowDecoration.getLeash()
+
+ fun dispose() {
+ if (isInitialised) resizeVeil.dispose()
+ }
+ }
+
+ private fun isTilingFocusRemoved(taskInfo: RunningTaskInfo): Boolean {
+ return taskInfo.isFocused &&
+ isTilingFocused &&
+ taskInfo.taskId != leftTaskResizingHelper?.taskInfo?.taskId &&
+ taskInfo.taskId != rightTaskResizingHelper?.taskInfo?.taskId
+ }
+
+ override fun onFocusTaskChanged(taskInfo: RunningTaskInfo?) {
+ if (taskInfo != null) {
+ moveTiledPairToFront(taskInfo)
+ }
+ }
+
+ private fun isTilingRefocused(taskInfo: RunningTaskInfo): Boolean {
+ return !isTilingFocused &&
+ taskInfo.isFocused &&
+ (taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId ||
+ taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId)
+ }
+
+ private fun buildTiledTasksMoveToFront(leftOnTop: Boolean): WindowContainerTransaction {
+ val wct = WindowContainerTransaction()
+ val leftTiledTask = leftTaskResizingHelper ?: return wct
+ val rightTiledTask = rightTaskResizingHelper ?: return wct
+ if (leftOnTop) {
+ wct.reorder(rightTiledTask.taskInfo.token, true)
+ wct.reorder(leftTiledTask.taskInfo.token, true)
+ } else {
+ wct.reorder(leftTiledTask.taskInfo.token, true)
+ wct.reorder(rightTiledTask.taskInfo.token, true)
+ }
+ return wct
+ }
+
+ fun removeTaskIfTiled(
+ taskId: Int,
+ taskVanished: Boolean = false,
+ shouldDelayUpdate: Boolean = false,
+ ) {
+ if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
+ removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate)
+ leftTaskResizingHelper = null
+ rightTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+ tearDownTiling()
+ return
+ }
+
+ if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+ removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate)
+ rightTaskResizingHelper = null
+ leftTaskResizingHelper
+ ?.desktopModeWindowDecoration
+ ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate)
+ tearDownTiling()
+ }
+ }
+
+ fun onUserChange() {
+ if (leftTaskResizingHelper != null) {
+ removeTask(leftTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true)
+ leftTaskResizingHelper = null
+ }
+ if (rightTaskResizingHelper != null) {
+ removeTask(rightTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true)
+ rightTaskResizingHelper = null
+ }
+ tearDownTiling()
+ }
+
+ private fun removeTask(
+ appResizingHelper: AppResizingHelper?,
+ taskVanished: Boolean = false,
+ shouldDelayUpdate: Boolean,
+ ) {
+ if (appResizingHelper == null) return
+ if (!taskVanished) {
+ appResizingHelper.desktopModeWindowDecoration.removeDragResizeListener(this)
+ appResizingHelper.desktopModeWindowDecoration.updateDisabledResizingEdge(
+ NONE,
+ shouldDelayUpdate,
+ )
+ }
+ appResizingHelper.dispose()
+ }
+
+ fun onOverviewAnimationStateChange(isRunning: Boolean) {
+ if (!isTilingManagerInitialised) return
+
+ if (isRunning) {
+ desktopTilingDividerWindowManager?.hideDividerBar()
+ } else if (allTiledTasksVisible()) {
+ desktopTilingDividerWindowManager?.showDividerBar()
+ }
+ }
+
+ override fun onTaskVanished(taskInfo: RunningTaskInfo?) {
+ val taskId = taskInfo?.taskId ?: return
+ removeTaskIfTiled(taskId, taskVanished = true, shouldDelayUpdate = true)
+ }
+
+ fun moveTiledPairToFront(taskInfo: RunningTaskInfo): Boolean {
+ if (!isTilingManagerInitialised) return false
+
+ // If a task that isn't tiled is being focused, let the generic handler do the work.
+ if (isTilingFocusRemoved(taskInfo)) {
+ isTilingFocused = false
+ return false
+ }
+
+ val leftTiledTask = leftTaskResizingHelper ?: return false
+ val rightTiledTask = rightTaskResizingHelper ?: return false
+ if (!allTiledTasksVisible()) return false
+ val isLeftOnTop = taskInfo.taskId == leftTiledTask.taskInfo.taskId
+ if (isTilingRefocused(taskInfo)) {
+ val t = transactionSupplier.get()
+ isTilingFocused = true
+ if (taskInfo.taskId == leftTaskResizingHelper?.taskInfo?.taskId) {
+ desktopTilingDividerWindowManager?.onRelativeLeashChanged(
+ leftTiledTask.getLeash(),
+ t,
+ )
+ }
+ if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) {
+ desktopTilingDividerWindowManager?.onRelativeLeashChanged(
+ rightTiledTask.getLeash(),
+ t,
+ )
+ }
+ transitions.startTransition(
+ TRANSIT_TO_FRONT,
+ buildTiledTasksMoveToFront(isLeftOnTop),
+ null,
+ )
+ t.apply()
+ return true
+ }
+ return false
+ }
+
+ private fun allTiledTasksVisible(): Boolean {
+ val leftTiledTask = leftTaskResizingHelper ?: return false
+ val rightTiledTask = rightTaskResizingHelper ?: return false
+ return taskRepository.isVisibleTask(leftTiledTask.taskInfo.taskId) &&
+ taskRepository.isVisibleTask(rightTiledTask.taskInfo.taskId)
+ }
+
+ private fun isResizeWithinSizeConstraints(
+ newLeftBounds: Rect,
+ newRightBounds: Rect,
+ leftBounds: Rect,
+ rightBounds: Rect,
+ stableBounds: Rect,
+ ): Boolean {
+ return DragPositioningCallbackUtility.isExceedingWidthConstraint(
+ newLeftBounds.width(),
+ leftBounds.width(),
+ stableBounds,
+ displayController,
+ leftTaskResizingHelper?.desktopModeWindowDecoration,
+ ) ||
+ DragPositioningCallbackUtility.isExceedingWidthConstraint(
+ newRightBounds.width(),
+ rightBounds.width(),
+ stableBounds,
+ displayController,
+ rightTaskResizingHelper?.desktopModeWindowDecoration,
+ )
+ }
+
+ private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect {
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return Rect()
+
+ val stableBounds = Rect()
+ displayLayout.getStableBounds(stableBounds)
+ val leftTiledTask = leftTaskResizingHelper
+ val rightTiledTask = rightTaskResizingHelper
+ val destinationWidth = stableBounds.width() / 2
+ return when (position) {
+ SnapPosition.LEFT -> {
+ val rightBound =
+ if (rightTiledTask == null) {
+ stableBounds.left + destinationWidth -
+ context.resources.getDimensionPixelSize(
+ R.dimen.split_divider_bar_width
+ ) / 2
+ } else {
+ rightTiledTask.bounds.left -
+ context.resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+ }
+ Rect(stableBounds.left, stableBounds.top, rightBound, stableBounds.bottom)
+ }
+
+ SnapPosition.RIGHT -> {
+ val leftBound =
+ if (leftTiledTask == null) {
+ stableBounds.right - destinationWidth +
+ context.resources.getDimensionPixelSize(
+ R.dimen.split_divider_bar_width
+ ) / 2
+ } else {
+ leftTiledTask.bounds.right +
+ context.resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+ }
+ Rect(leftBound, stableBounds.top, stableBounds.right, stableBounds.bottom)
+ }
+ }
+ }
+
+ private fun inflateDividerBounds(displayLayout: DisplayLayout): Rect {
+ val stableBounds = Rect()
+ displayLayout.getStableBounds(stableBounds)
+
+ val leftDividerBounds = leftTaskResizingHelper?.bounds?.right ?: return Rect()
+ val rightDividerBounds = rightTaskResizingHelper?.bounds?.left ?: return Rect()
+
+ // Bounds should never be null here, so assertion is necessary otherwise it's illegal state.
+ return Rect(leftDividerBounds, stableBounds.top, rightDividerBounds, stableBounds.bottom)
+ }
+
+ private fun tearDownTiling() {
+ if (isTilingManagerInitialised) shellTaskOrganizer.removeFocusListener(this)
+
+ if (leftTaskResizingHelper == null && rightTaskResizingHelper == null) {
+ shellTaskOrganizer.removeTaskVanishedListener(this)
+ }
+ isTilingFocused = false
+ isTilingManagerInitialised = false
+ desktopTilingDividerWindowManager?.release()
+ desktopTilingDividerWindowManager = null
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.kt
new file mode 100644
index 0000000..b3b30ad
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DividerMoveCallback.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+/** Divider move callback to whichever entity that handles the moving logic. */
+interface DividerMoveCallback {
+ /** Called on the divider move start gesture. */
+ fun onDividerMoveStart(pos: Int)
+
+ /** Called on the divider moved by dragging it. */
+ fun onDividerMove(pos: Int): Boolean
+
+ /** Called on divider move gesture end. */
+ fun onDividerMovedEnd(pos: Int)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
new file mode 100644
index 0000000..065a5d7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.provider.DeviceConfig
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.PointerIcon
+import android.view.View
+import android.view.ViewConfiguration
+import android.widget.FrameLayout
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.wm.shell.R
+import com.android.wm.shell.common.split.DividerHandleView
+import com.android.wm.shell.common.split.DividerRoundedCorner
+import com.android.wm.shell.shared.animation.Interpolators
+import com.android.wm.shell.windowdecor.DragDetector
+
+/** Divider for tiling split screen, currently mostly a copy of [DividerView]. */
+class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.MotionEventHandler {
+ private val paint = Paint()
+ private val backgroundRect = Rect()
+
+ private lateinit var callback: DividerMoveCallback
+ private lateinit var handle: DividerHandleView
+ private lateinit var corners: DividerRoundedCorner
+ private var touchElevation = 0
+
+ private var moving = false
+ private var startPos = 0
+ var handleRegionWidth: Int = 0
+ private var handleRegionHeight = 0
+ private var lastAcceptedPos = 0
+ @VisibleForTesting
+ var handleStartY = 0
+ @VisibleForTesting
+ var handleEndY = 0
+ private var canResize = false
+ /**
+ * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with
+ * insets.
+ */
+ private val dividerBounds = Rect()
+ private var dividerBar: FrameLayout? = null
+ private lateinit var dragDetector: DragDetector
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ ) : super(context, attrs, defStyleAttr)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ defStyleRes: Int,
+ ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ /** Sets up essential dependencies of the divider bar. */
+ fun setup(dividerMoveCallback: DividerMoveCallback, dividerBounds: Rect) {
+ callback = dividerMoveCallback
+ this.dividerBounds.set(dividerBounds)
+ handle.setIsLeftRightSplit(true)
+ corners.setIsLeftRightSplit(true)
+ handleRegionHeight =
+ resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_width)
+
+ handleRegionWidth =
+ resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_height)
+ initHandleYCoordinates()
+ dragDetector =
+ DragDetector(
+ this,
+ /* holdToDragMinDurationMs= */ 0,
+ ViewConfiguration.get(mContext).scaledTouchSlop,
+ )
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ dividerBar = requireViewById(R.id.divider_bar)
+ handle = requireViewById(R.id.docked_divider_handle)
+ corners = requireViewById(R.id.docked_divider_rounded_corner)
+ touchElevation =
+ resources.getDimensionPixelSize(R.dimen.docked_stack_divider_lift_elevation)
+ setOnTouchListener(this)
+ setWillNotDraw(false)
+ paint.color = resources.getColor(R.color.split_divider_background, null)
+ paint.isAntiAlias = true
+ paint.style = Paint.Style.FILL
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ if (changed) {
+ val dividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width)
+ val backgroundLeft = (width - dividerSize) / 2
+ val backgroundTop = 0
+ val backgroundRight = left + dividerSize
+ val backgroundBottom = height
+ backgroundRect.set(backgroundLeft, backgroundTop, backgroundRight, backgroundBottom)
+ }
+ }
+
+ override fun onResolvePointerIcon(event: MotionEvent, pointerIndex: Int): PointerIcon =
+ PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW)
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean =
+ dragDetector.onMotionEvent(v, event)
+
+ private fun setTouching() {
+ handle.setTouching(true, true)
+ // Lift handle as well so it doesn't get behind the background, even though it doesn't
+ // cast shadow.
+ handle
+ .animate()
+ .setInterpolator(Interpolators.TOUCH_RESPONSE)
+ .setDuration(TOUCH_ANIMATION_DURATION)
+ .translationZ(touchElevation.toFloat())
+ .start()
+ }
+
+ private fun releaseTouching() {
+ handle.setTouching(false, true)
+ handle
+ .animate()
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+ .translationZ(0f)
+ .start()
+ }
+
+ override fun onHoverEvent(event: MotionEvent): Boolean {
+ if (
+ !DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.CURSOR_HOVER_STATES_ENABLED,
+ /* defaultValue = */ false,
+ )
+ ) {
+ return false
+ }
+
+ if (event.action == MotionEvent.ACTION_HOVER_ENTER) {
+ setHovering()
+ return true
+ }
+ if (event.action == MotionEvent.ACTION_HOVER_EXIT) {
+ releaseHovering()
+ return true
+ }
+ return false
+ }
+
+ @VisibleForTesting
+ fun setHovering() {
+ handle.setHovering(true, true)
+ handle
+ .animate()
+ .setInterpolator(Interpolators.TOUCH_RESPONSE)
+ .setDuration(TOUCH_ANIMATION_DURATION)
+ .translationZ(touchElevation.toFloat())
+ .start()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.drawRect(backgroundRect, paint)
+ }
+
+ @VisibleForTesting
+ fun releaseHovering() {
+ handle.setHovering(false, true)
+ handle
+ .animate()
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
+ .translationZ(0f)
+ .start()
+ }
+
+ override fun handleMotionEvent(v: View?, event: MotionEvent): Boolean {
+ val touchPos = event.rawX.toInt()
+ val yTouchPosInDivider = event.y.toInt()
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ if (!isWithinHandleRegion(yTouchPosInDivider)) return true
+ callback.onDividerMoveStart(touchPos)
+ setTouching()
+ startPos = touchPos
+ canResize = true
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ if (!canResize) return true
+ if (!moving) {
+ startPos = touchPos
+ moving = true
+ }
+
+ val pos = dividerBounds.left + touchPos - startPos
+ if (callback.onDividerMove(pos)) {
+ lastAcceptedPos = touchPos
+ }
+ }
+
+ MotionEvent.ACTION_CANCEL,
+ MotionEvent.ACTION_UP -> {
+ if (!canResize) return true
+ dividerBounds.left = dividerBounds.left + lastAcceptedPos - startPos
+ if (moving) {
+ callback.onDividerMovedEnd(dividerBounds.left)
+ moving = false
+ canResize = false
+ }
+
+ releaseTouching()
+ }
+ }
+ return true
+ }
+
+ private fun isWithinHandleRegion(touchYPos: Int): Boolean {
+ return touchYPos in handleStartY..handleEndY
+ }
+
+ private fun initHandleYCoordinates() {
+ handleStartY = (dividerBounds.height() - handleRegionHeight) / 2
+ handleEndY = handleStartY + handleRegionHeight
+ }
+
+ companion object {
+ const val TOUCH_ANIMATION_DURATION: Long = 150
+ const val TOUCH_RELEASE_ANIMATION_DURATION: Long = 200
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index bc2b36c..0a6dfbf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -128,6 +128,8 @@
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.function.Consumer
@@ -223,6 +225,10 @@
@Mock lateinit var motionEvent: MotionEvent
private lateinit var mockitoSession: StaticMockitoSession
+ @Mock
+ private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
+ @Mock
+ private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration
private lateinit var controller: DesktopTasksController
private lateinit var shellInit: ShellInit
private lateinit var taskRepository: DesktopRepository
@@ -336,6 +342,7 @@
mockInputManager,
mockFocusTransitionObserver,
desktopModeEventLogger,
+ desktopTilingDecorViewModel,
)
}
@@ -2835,7 +2842,9 @@
Rect(100, -100, 500, 1000), /* currentDragBounds */
Rect(0, 50, 2000, 2000), /* validDragArea */
Rect() /* dragStartBounds */,
- motionEvent)
+ motionEvent,
+ desktopWindowDecoration,
+ )
val rectAfterEnd = Rect(100, 50, 500, 1150)
verify(transitions)
.startTransition(
@@ -2871,7 +2880,9 @@
currentDragBounds, /* currentDragBounds */
Rect(0, 50, 2000, 2000) /* validDragArea */,
Rect() /* dragStartBounds */,
- motionEvent)
+ motionEvent,
+ desktopWindowDecoration,
+ )
verify(transitions)
@@ -3135,6 +3146,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() {
val bounds = Rect(100, 100, 300, 300)
val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply {
@@ -3150,7 +3162,8 @@
STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom
)
- controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
+ controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT,
+ ResizeTrigger.SNAP_LEFT_MENU, motionEvent, desktopWindowDecoration)
// Assert bounds set to stable bounds
val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds)
@@ -3165,6 +3178,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING)
fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() {
// Set up task to already be in snapped-left bounds
val bounds = Rect(
@@ -3180,8 +3194,8 @@
// Attempt to snap left again
val currentDragBounds = Rect(bounds).apply { offset(-100, 0) }
- controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, motionEvent)
-
+ controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT,
+ ResizeTrigger.SNAP_LEFT_MENU, motionEvent, desktopWindowDecoration)
// Assert that task is NOT updated via WCT
verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any())
@@ -3204,7 +3218,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING)
+ @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, Flags.FLAG_ENABLE_TILE_RESIZING)
fun handleSnapResizingTask_nonResizable_snapsToHalfScreen() {
val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply {
isResizeable = false
@@ -3215,7 +3229,9 @@
Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom)
controller.handleSnapResizingTask(
- task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent
+
+ task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent,
+ desktopWindowDecoration
)
val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds)
assertThat(findBoundsChange(wct, task)).isEqualTo(
@@ -3239,7 +3255,8 @@
val currentDragBounds = Rect(0, 100, 300, 500)
controller.handleSnapResizingTask(
- task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent)
+ task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent,
+ desktopWindowDecoration)
verify(mReturnToDragStartAnimator).start(
eq(task.taskId),
eq(mockSurface),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index f95b0d1..8dd1545 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -31,6 +31,7 @@
import android.app.ActivityManager;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.SurfaceControl;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -49,6 +50,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -65,6 +67,9 @@
@RunWith(AndroidJUnit4.class)
public final class FreeformTaskListenerTests extends ShellTestCase {
+ @Rule
+ public final SetFlagsRule setFlagsRule = new SetFlagsRule();
+
@Mock
private ShellTaskOrganizer mTaskOrganizer;
@Mock
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index c5526fc..c42be7f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -666,7 +666,8 @@
eq(currentBounds),
eq(SnapPosition.LEFT),
eq(ResizeTrigger.SNAP_LEFT_MENU),
- eq(null)
+ eq(null),
+ eq(decor)
)
assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface)
}
@@ -706,7 +707,8 @@
eq(currentBounds),
eq(SnapPosition.LEFT),
eq(ResizeTrigger.SNAP_LEFT_MENU),
- eq(null)
+ eq(null),
+ eq(decor),
)
assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
}
@@ -727,7 +729,9 @@
verify(mockDesktopTasksController, never())
.snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT),
eq(ResizeTrigger.MAXIMIZE_BUTTON),
- eq(null))
+ eq(null),
+ eq(decor),
+ )
verify(mockToast).show()
}
@@ -750,7 +754,8 @@
eq(currentBounds),
eq(SnapPosition.RIGHT),
eq(ResizeTrigger.SNAP_RIGHT_MENU),
- eq(null)
+ eq(null),
+ eq(decor),
)
assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
}
@@ -790,7 +795,8 @@
eq(currentBounds),
eq(SnapPosition.RIGHT),
eq(ResizeTrigger.SNAP_RIGHT_MENU),
- eq(null)
+ eq(null),
+ eq(decor),
)
assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue)
}
@@ -811,7 +817,9 @@
verify(mockDesktopTasksController, never())
.snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT),
eq(ResizeTrigger.MAXIMIZE_BUTTON),
- eq(null))
+ eq(null),
+ eq(decor),
+ )
verify(mockToast).show()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
index 57469bf..e7d328e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -65,7 +65,7 @@
private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10;
private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry(
TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET,
- FINE_CORNER_SIZE, LARGE_CORNER_SIZE);
+ FINE_CORNER_SIZE, LARGE_CORNER_SIZE, DragResizeWindowGeometry.DisabledEdge.NONE);
// Points in the edge resize handle. Note that coordinates start from the top left.
private static final Point TOP_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2,
-EDGE_RESIZE_THICKNESS / 2);
@@ -100,23 +100,25 @@
GEOMETRY,
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET, FINE_CORNER_SIZE,
- LARGE_CORNER_SIZE))
+ LARGE_CORNER_SIZE, DragResizeWindowGeometry.DisabledEdge.NONE))
.addEqualityGroup(
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
- FINE_CORNER_SIZE, LARGE_CORNER_SIZE),
+ FINE_CORNER_SIZE, LARGE_CORNER_SIZE,
+ DragResizeWindowGeometry.DisabledEdge.NONE),
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
- FINE_CORNER_SIZE, LARGE_CORNER_SIZE))
+ FINE_CORNER_SIZE, LARGE_CORNER_SIZE,
+ DragResizeWindowGeometry.DisabledEdge.NONE))
.addEqualityGroup(
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
FINE_CORNER_SIZE,
- LARGE_CORNER_SIZE + 5),
+ LARGE_CORNER_SIZE + 5, DragResizeWindowGeometry.DisabledEdge.NONE),
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, EDGE_RESIZE_HANDLE_INSET,
FINE_CORNER_SIZE,
- LARGE_CORNER_SIZE + 5))
+ LARGE_CORNER_SIZE + 5, DragResizeWindowGeometry.DisabledEdge.NONE))
.testEquals();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index ca1f9ab..3b80cb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -47,6 +47,7 @@
import java.util.function.Supplier
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
+import org.mockito.kotlin.times
import org.mockito.Mockito.`when` as whenever
/**
@@ -66,7 +67,7 @@
@Mock
private lateinit var mockWindowDecoration: WindowDecoration<*>
@Mock
- private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
+ private lateinit var mockDragEventListener: DragPositioningCallbackUtility.DragEventListener
@Mock
private lateinit var taskToken: WindowContainerToken
@@ -140,7 +141,7 @@
mockTransitions,
mockWindowDecoration,
mockDisplayController,
- mockDragStartListener,
+ mockDragEventListener,
mockTransactionFactory
)
}
@@ -220,6 +221,7 @@
change.configuration.windowConfiguration.bounds == rectAfterMove
}
})
+ verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID))
taskPositioner.onDragPositioningEnd(
STARTING_BOUNDS.left.toFloat() + 10,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 1dfbd67..e7df864 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -79,7 +79,7 @@
@Mock
private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration
@Mock
- private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener
+ private lateinit var mockDragEventListener: DragPositioningCallbackUtility.DragEventListener
@Mock
private lateinit var taskToken: WindowContainerToken
@@ -156,7 +156,7 @@
mockShellTaskOrganizer,
mockDesktopWindowDecoration,
mockDisplayController,
- mockDragStartListener,
+ mockDragEventListener,
mockTransactionFactory,
mockTransitions,
mockInteractionJankMonitor,
@@ -433,6 +433,7 @@
// isResizingOrAnimating should be set to true after move during a resize
Assert.assertTrue(taskPositioner.isResizingOrAnimating)
+ verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID))
taskPositioner.onDragPositioningEnd(
STARTING_BOUNDS.left.toFloat(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
new file mode 100644
index 0000000..0ccd424
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+import android.content.Context
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingDecorViewModelTest : ShellTestCase() {
+ private val contextMock: Context = mock()
+ private val displayControllerMock: DisplayController = mock()
+ private val rootTdaOrganizerMock: RootTaskDisplayAreaOrganizer = mock()
+ private val syncQueueMock: SyncTransactionQueue = mock()
+ private val transitionsMock: Transitions = mock()
+ private val shellTaskOrganizerMock: ShellTaskOrganizer = mock()
+ private val desktopRepository: DesktopRepository = mock()
+ private val toggleResizeDesktopTaskTransitionHandlerMock:
+ ToggleResizeDesktopTaskTransitionHandler =
+ mock()
+ private val returnToDragStartAnimatorMock: ReturnToDragStartAnimator = mock()
+
+ private val desktopModeWindowDecorationMock: DesktopModeWindowDecoration = mock()
+ private val desktopTilingDecoration: DesktopTilingWindowDecoration = mock()
+ private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel
+
+ @Before
+ fun setUp() {
+ desktopTilingDecorViewModel =
+ DesktopTilingDecorViewModel(
+ contextMock,
+ displayControllerMock,
+ rootTdaOrganizerMock,
+ syncQueueMock,
+ transitionsMock,
+ shellTaskOrganizerMock,
+ toggleResizeDesktopTaskTransitionHandlerMock,
+ returnToDragStartAnimatorMock,
+ desktopRepository,
+ )
+ }
+
+ @Test
+ fun testTiling_shouldCreate_newTilingDecoration() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ task1.displayId = 1
+ task2.displayId = 2
+
+ desktopTilingDecorViewModel.snapToHalfScreen(
+ task1,
+ desktopModeWindowDecorationMock,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ assertThat(desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.size())
+ .isEqualTo(1)
+ desktopTilingDecorViewModel.snapToHalfScreen(
+ task2,
+ desktopModeWindowDecorationMock,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ assertThat(desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.size())
+ .isEqualTo(2)
+ }
+
+ @Test
+ fun removeTile_shouldCreate_newTilingDecoration() {
+ val task1 = createFreeformTask()
+ task1.displayId = 1
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 1,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.removeTaskIfTiled(task1.displayId, task1.taskId)
+
+ verify(desktopTilingDecoration, times(1)).removeTaskIfTiled(any(), any(), any())
+ }
+
+ @Test
+ fun moveTaskToFront_shouldRoute_toCorrectTilingDecoration() {
+
+ val task1 = createFreeformTask()
+ task1.displayId = 1
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 1,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.moveTaskToFrontIfTiled(task1)
+
+ verify(desktopTilingDecoration, times(1)).moveTiledPairToFront(any())
+ }
+
+ @Test
+ fun overviewAnimation_starting_ShouldNotifyAllDecorations() {
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 1,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 2,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.onOverviewAnimationStateChange(true)
+
+ verify(desktopTilingDecoration, times(2)).onOverviewAnimationStateChange(any())
+ }
+
+ @Test
+ fun userChange_starting_allTilingSessionsShouldBeDestroyed() {
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 1,
+ desktopTilingDecoration,
+ )
+ desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put(
+ 2,
+ desktopTilingDecoration,
+ )
+
+ desktopTilingDecorViewModel.onUserChange()
+
+ verify(desktopTilingDecoration, times(2)).onUserChange()
+ }
+
+ companion object {
+ private val BOUNDS = Rect(1, 2, 3, 4)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
new file mode 100644
index 0000000..0ee3f46
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.SyncTransactionQueue
+import java.util.function.Supplier
+import kotlin.test.Test
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingDividerWindowManagerTest : ShellTestCase() {
+ private lateinit var config: Configuration
+
+ private var windowName: String = "Tiling"
+
+ private val leashMock = mock<SurfaceControl>()
+
+ private val syncQueueMock = mock<SyncTransactionQueue>()
+
+ private val transitionHandlerMock = mock<DesktopTilingWindowDecoration>()
+
+ private val transactionSupplierMock = mock<Supplier<SurfaceControl.Transaction>>()
+
+ private val surfaceControl = mock<SurfaceControl>()
+
+ private val transaction = mock<SurfaceControl.Transaction>()
+
+ private lateinit var desktopTilingWindowManager: DesktopTilingDividerWindowManager
+
+ @Before
+ fun setup() {
+ config = Configuration()
+ config.setToDefaults()
+ desktopTilingWindowManager =
+ DesktopTilingDividerWindowManager(
+ config,
+ windowName,
+ mContext,
+ leashMock,
+ syncQueueMock,
+ transitionHandlerMock,
+ transactionSupplierMock,
+ BOUNDS,
+ )
+ }
+
+ @Test
+ @UiThreadTest
+ fun testWindowManager_isInitialisedAndReleased() {
+ whenever(transactionSupplierMock.get()).thenReturn(transaction)
+ whenever(transaction.hide(any())).thenReturn(transaction)
+ whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction)
+ whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction)
+ whenever(transaction.remove(any())).thenReturn(transaction)
+
+ desktopTilingWindowManager.generateViewHost(surfaceControl)
+
+ // Ensure a surfaceControl transaction runs to show the divider.
+ verify(transactionSupplierMock, times(1)).get()
+ verify(syncQueueMock, times(1)).runInSync(any())
+
+ desktopTilingWindowManager.release()
+ verify(transaction, times(1)).hide(any())
+ verify(transaction, times(1)).remove(any())
+ verify(transaction, times(1)).apply()
+ }
+
+ companion object {
+ private val BOUNDS = Rect(1, 2, 3, 4)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
new file mode 100644
index 0000000..0b04a21
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.tiling
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator
+import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
+import com.android.wm.shell.windowdecor.DragResizeWindowGeometry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Captor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.capture
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTilingWindowDecorationTest : ShellTestCase() {
+
+ private val context: Context = mock()
+
+ private val syncQueue: SyncTransactionQueue = mock()
+
+ private val displayController: DisplayController = mock()
+ private val displayId: Int = 0
+
+ private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer = mock()
+
+ private val transitions: Transitions = mock()
+
+ private val shellTaskOrganizer: ShellTaskOrganizer = mock()
+
+ private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler =
+ mock()
+
+ private val returnToDragStartAnimator: ReturnToDragStartAnimator = mock()
+
+ private val desktopWindowDecoration: DesktopModeWindowDecoration = mock()
+
+ private val displayLayout: DisplayLayout = mock()
+
+ private val resources: Resources = mock()
+ private val surfaceControlMock: SurfaceControl = mock()
+ private val transaction: SurfaceControl.Transaction = mock()
+ private val tiledTaskHelper: DesktopTilingWindowDecoration.AppResizingHelper = mock()
+ private val transition: IBinder = mock()
+ private val info: TransitionInfo = mock()
+ private val finishCallback: Transitions.TransitionFinishCallback = mock()
+ private val desktopRepository: DesktopRepository = mock()
+ private val desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager = mock()
+ private lateinit var tilingDecoration: DesktopTilingWindowDecoration
+
+ private val split_divider_width = 10
+
+ @Captor private lateinit var wctCaptor: ArgumentCaptor<WindowContainerTransaction>
+
+ @Before
+ fun setUp() {
+ tilingDecoration =
+ DesktopTilingWindowDecoration(
+ context,
+ syncQueue,
+ displayController,
+ displayId,
+ rootTdaOrganizer,
+ transitions,
+ shellTaskOrganizer,
+ toggleResizeDesktopTaskTransitionHandler,
+ returnToDragStartAnimator,
+ desktopRepository,
+ )
+ }
+
+ @Test
+ fun taskTiled_toCorrectBounds_leftTile() {
+ val task1 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+
+ verify(toggleResizeDesktopTaskTransitionHandler).startTransition(capture(wctCaptor), any())
+ for (change in wctCaptor.value.changes) {
+ val bounds = change.value.configuration.windowConfiguration.bounds
+ val leftBounds = getLeftTaskBounds()
+ assertRectEqual(bounds, leftBounds)
+ }
+ }
+
+ @Test
+ fun taskTiled_toCorrectBounds_rightTile() {
+ // Setup
+ val task1 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+
+ verify(toggleResizeDesktopTaskTransitionHandler).startTransition(capture(wctCaptor), any())
+ for (change in wctCaptor.value.changes) {
+ val bounds = change.value.configuration.windowConfiguration.bounds
+ val leftBounds = getRightTaskBounds()
+ assertRectEqual(bounds, leftBounds)
+ }
+ }
+
+ @Test
+ fun taskTiled_notAnimated_whenTilingPositionNotChange() {
+ val task1 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(desktopWindowDecoration.getLeash()).thenReturn(surfaceControlMock)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ task1.configuration.windowConfiguration.setBounds(getLeftTaskBounds())
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ NON_STABLE_BOUNDS_MOCK,
+ )
+
+ verify(toggleResizeDesktopTaskTransitionHandler, times(1))
+ .startTransition(capture(wctCaptor), any())
+ verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), any())
+ for (change in wctCaptor.value.changes) {
+ val bounds = change.value.configuration.windowConfiguration.bounds
+ val leftBounds = getLeftTaskBounds()
+ assertRectEqual(bounds, leftBounds)
+ }
+ }
+
+ @Test
+ fun taskNotTiled_notBroughtToFront_tilingNotInitialised() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+
+ assertThat(tilingDecoration.moveTiledPairToFront(task2)).isFalse()
+ verify(transitions, never()).startTransition(any(), any(), any())
+ }
+
+ @Test
+ fun taskNotTiled_notBroughtToFront_taskNotTiled() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val task3 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+
+ assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse()
+ verify(transitions, never()).startTransition(any(), any(), any())
+ }
+
+ @Test
+ fun taskTiled_broughtToFront_alreadyInFrontNoAction() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ task1.isFocused = true
+
+ assertThat(tilingDecoration.moveTiledPairToFront(task1)).isFalse()
+ verify(transitions, never()).startTransition(any(), any(), any())
+ }
+
+ @Test
+ fun taskTiled_broughtToFront_bringToFront() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val task3 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(desktopWindowDecoration.getLeash()).thenReturn(surfaceControlMock)
+ whenever(desktopRepository.isVisibleTask(any())).thenReturn(true)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ task1.isFocused = true
+ task3.isFocused = true
+
+ assertThat(tilingDecoration.moveTiledPairToFront(task3)).isFalse()
+ assertThat(tilingDecoration.moveTiledPairToFront(task1)).isTrue()
+ verify(transitions, times(1)).startTransition(eq(TRANSIT_TO_FRONT), any(), eq(null))
+ }
+
+ @Test
+ fun taskTiledTasks_NotResized_BeforeTouchEndArrival() {
+ // Setup
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ desktopWindowDecoration.mTaskInfo = task1
+ task1.minWidth = 0
+ task1.minHeight = 0
+ initTiledTaskHelperMock(task1)
+ desktopWindowDecoration.mDecorWindowContext = context
+ whenever(resources.getBoolean(any())).thenReturn(true)
+
+ // Act
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+ tilingDecoration.rightTaskResizingHelper = tiledTaskHelper
+ tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+
+ // Assert
+ verify(transaction, times(1)).apply()
+ // Show should be called twice for each tiled app, to show the veil and the icon for each
+ // of them.
+ verify(tiledTaskHelper, times(2)).showVeil(any())
+
+ // Move again
+ tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+ verify(tiledTaskHelper, times(2)).updateVeil(any())
+ verify(transitions, never()).startTransition(any(), any(), any())
+
+ // End moving, no startTransition because bounds did not change.
+ tiledTaskHelper.newBounds.set(BOUNDS)
+ tilingDecoration.onDividerHandleDragEnd(BOUNDS, transaction)
+ verify(tiledTaskHelper, times(2)).hideVeil()
+ verify(transitions, never()).startTransition(any(), any(), any())
+
+ // Move then end again with bounds changing to ensure startTransition is called.
+ tilingDecoration.onDividerHandleMoved(BOUNDS, transaction)
+ tilingDecoration.onDividerHandleDragEnd(BOUNDS, transaction)
+ verify(transitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), any(), eq(tilingDecoration))
+ // No hide veil until start animation is called.
+ verify(tiledTaskHelper, times(2)).hideVeil()
+
+ tilingDecoration.startAnimation(transition, info, transaction, transaction, finishCallback)
+ // the startAnimation function should hide the veils.
+ verify(tiledTaskHelper, times(4)).hideVeil()
+ }
+
+ @Test
+ fun taskTiled_shouldBeRemoved_whenTileBroken() {
+ val task1 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+
+ tilingDecoration.removeTaskIfTiled(task1.taskId)
+
+ assertThat(tilingDecoration.leftTaskResizingHelper).isNull()
+ verify(desktopWindowDecoration, times(1)).removeDragResizeListener(any())
+ verify(desktopWindowDecoration, times(1))
+ .updateDisabledResizingEdge(eq(DragResizeWindowGeometry.DisabledEdge.NONE), eq(false))
+ verify(tiledTaskHelper, times(1)).dispose()
+ }
+
+ @Test
+ fun taskNotTiled_shouldNotBeRemoved_whenNotTiled() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+
+ tilingDecoration.removeTaskIfTiled(task2.taskId)
+
+ assertThat(tilingDecoration.leftTaskResizingHelper).isNotNull()
+ verify(desktopWindowDecoration, never()).removeDragResizeListener(any())
+ verify(desktopWindowDecoration, never()).updateDisabledResizingEdge(any(), any())
+ verify(tiledTaskHelper, never()).dispose()
+ }
+
+ @Test
+ fun tasksTiled_shouldBeRemoved_whenSessionDestroyed() {
+ val task1 = createFreeformTask()
+ val task2 = createFreeformTask()
+ val stableBounds = STABLE_BOUNDS_MOCK
+ whenever(displayController.getDisplayLayout(any())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(stableBounds)
+ }
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.getDimensionPixelSize(any())).thenReturn(split_divider_width)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(task1)
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ tilingDecoration.onAppTiled(
+ task1,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.LEFT,
+ BOUNDS,
+ )
+ tilingDecoration.onAppTiled(
+ task2,
+ desktopWindowDecoration,
+ DesktopTasksController.SnapPosition.RIGHT,
+ BOUNDS,
+ )
+ tilingDecoration.leftTaskResizingHelper = tiledTaskHelper
+ tilingDecoration.rightTaskResizingHelper = tiledTaskHelper
+ tilingDecoration.desktopTilingDividerWindowManager = desktopTilingDividerWindowManager
+
+ tilingDecoration.onUserChange()
+
+ assertThat(tilingDecoration.leftTaskResizingHelper).isNull()
+ assertThat(tilingDecoration.rightTaskResizingHelper).isNull()
+ verify(desktopWindowDecoration, times(2)).removeDragResizeListener(any())
+ verify(tiledTaskHelper, times(2)).dispose()
+ }
+
+ private fun initTiledTaskHelperMock(taskInfo: ActivityManager.RunningTaskInfo) {
+ whenever(tiledTaskHelper.bounds).thenReturn(BOUNDS)
+ whenever(tiledTaskHelper.taskInfo).thenReturn(taskInfo)
+ whenever(tiledTaskHelper.newBounds).thenReturn(Rect(BOUNDS))
+ whenever(tiledTaskHelper.desktopModeWindowDecoration).thenReturn(desktopWindowDecoration)
+ }
+
+ private fun assertRectEqual(rect1: Rect, rect2: Rect) {
+ assertThat(rect1.left).isEqualTo(rect2.left)
+ assertThat(rect1.right).isEqualTo(rect2.right)
+ assertThat(rect1.top).isEqualTo(rect2.top)
+ assertThat(rect1.bottom).isEqualTo(rect2.bottom)
+ return
+ }
+
+ private fun getRightTaskBounds(): Rect {
+ val stableBounds = STABLE_BOUNDS_MOCK
+ val destinationWidth = stableBounds.width() / 2
+ val leftBound = stableBounds.right - destinationWidth + split_divider_width / 2
+ return Rect(leftBound, stableBounds.top, stableBounds.right, stableBounds.bottom)
+ }
+
+ private fun getLeftTaskBounds(): Rect {
+ val stableBounds = STABLE_BOUNDS_MOCK
+ val destinationWidth = stableBounds.width() / 2
+ val rightBound = stableBounds.left + destinationWidth - split_divider_width / 2
+ return Rect(stableBounds.left, stableBounds.top, rightBound, stableBounds.bottom)
+ }
+
+ companion object {
+ private val NON_STABLE_BOUNDS_MOCK = Rect(50, 55, 100, 100)
+ private val STABLE_BOUNDS_MOCK = Rect(0, 0, 100, 100)
+ private val BOUNDS = Rect(1, 2, 3, 4)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
new file mode 100644
index 0000000..fd5eb88
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.tiling
+
+import android.graphics.Rect
+import android.os.SystemClock
+import android.testing.AndroidTestingRunner
+import android.view.InputDevice
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TilingDividerViewTest : ShellTestCase() {
+
+ private lateinit var tilingDividerView: TilingDividerView
+
+ private val dividerMoveCallbackMock = mock<DividerMoveCallback>()
+
+ private val viewMock = mock<View>()
+
+ @Before
+ @UiThreadTest
+ fun setUp() {
+ tilingDividerView =
+ LayoutInflater.from(mContext).inflate(R.layout.tiling_split_divider, /* root= */ null)
+ as TilingDividerView
+ tilingDividerView.setup(dividerMoveCallbackMock, BOUNDS)
+ tilingDividerView.handleStartY = 0
+ tilingDividerView.handleEndY = 1500
+ }
+
+ @Test
+ @UiThreadTest
+ fun testCallbackOnTouch() {
+ val x = 5
+ val y = 5
+ val downTime: Long = SystemClock.uptimeMillis()
+
+ val downMotionEvent =
+ getMotionEvent(downTime, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat())
+ tilingDividerView.handleMotionEvent(viewMock, downMotionEvent)
+ verify(dividerMoveCallbackMock, times(1)).onDividerMoveStart(any())
+
+ val motionEvent =
+ getMotionEvent(downTime, MotionEvent.ACTION_MOVE, x.toFloat(), y.toFloat())
+ tilingDividerView.handleMotionEvent(viewMock, motionEvent)
+ verify(dividerMoveCallbackMock, times(1)).onDividerMove(any())
+
+ val upMotionEvent =
+ getMotionEvent(downTime, MotionEvent.ACTION_UP, x.toFloat(), y.toFloat())
+ tilingDividerView.handleMotionEvent(viewMock, upMotionEvent)
+ verify(dividerMoveCallbackMock, times(1)).onDividerMovedEnd(any())
+ }
+
+ private fun getMotionEvent(eventTime: Long, action: Int, x: Float, y: Float): MotionEvent {
+ val properties = MotionEvent.PointerProperties()
+ properties.id = 0
+ properties.toolType = MotionEvent.TOOL_TYPE_UNKNOWN
+
+ val coords = MotionEvent.PointerCoords()
+ coords.pressure = 1f
+ coords.size = 1f
+ coords.x = x
+ coords.y = y
+
+ return MotionEvent.obtain(
+ eventTime,
+ eventTime,
+ action,
+ 1,
+ arrayOf(properties),
+ arrayOf(coords),
+ 0,
+ 0,
+ 1.0f,
+ 1.0f,
+ 0,
+ 0,
+ InputDevice.SOURCE_TOUCHSCREEN,
+ 0,
+ )
+ }
+
+ companion object {
+ private val BOUNDS = Rect(0, 0, 1500, 1500)
+ }
+}
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 2c8e352..57f5f52c 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -316,7 +316,7 @@
@NonNull
public Builder setMediaProjection(@NonNull MediaProjection projection) {
if (projection == null) {
- throw new IllegalArgumentException("Invalid null volume callback");
+ throw new IllegalArgumentException("Invalid null media projection");
}
mProjection = projection;
return this;
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index 5a8763f..81a2e6a 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -159,3 +159,10 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "hearing_devices_ambient_volume_control"
+ namespace: "accessibility"
+ description: "Enable the ambient volume control in device details and hearing devices dialog."
+ bug: "357878944"
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3d2c931..2a65d39 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -435,9 +435,9 @@
}
flag {
- name: "status_bar_ron_chips"
+ name: "status_bar_notification_chips"
namespace: "systemui"
- description: "Show rich ongoing notifications as chips in the status bar"
+ description: "Show promoted ongoing notifications as chips in the status bar"
bug: "361346412"
}
@@ -991,6 +991,16 @@
}
flag {
+ name: "shortcut_helper_key_glyph"
+ namespace: "systemui"
+ description: "Allow showing key glyph in shortcut helper"
+ bug: "353902478"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "dream_overlay_bouncer_swipe_direction_filtering"
namespace: "systemui"
description: "do not initiate bouncer swipe when the direction is opposite of the expansion"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 476cced..e329aae 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -181,9 +181,11 @@
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.ui.viewmodel.ResizeInfo
+import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
import com.android.systemui.communal.util.DensityUtils.Companion.adjustedDp
import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView
import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import kotlin.math.max
@@ -665,6 +667,7 @@
maxHeightPx: Int,
modifier: Modifier = Modifier,
alpha: () -> Float = { 1f },
+ viewModel: ResizeableItemFrameViewModel,
onResize: (info: ResizeInfo) -> Unit = {},
content: @Composable (modifier: Modifier) -> Unit,
) {
@@ -680,6 +683,7 @@
enabled = enabled,
alpha = alpha,
modifier = modifier,
+ viewModel = viewModel,
onResize = onResize,
minHeightPx = minHeightPx,
maxHeightPx = maxHeightPx,
@@ -796,6 +800,14 @@
false
}
+ val resizeableItemFrameViewModel =
+ rememberViewModel(
+ key = item.size.span,
+ traceName = "ResizeableItemFrame.viewModel.$index",
+ ) {
+ ResizeableItemFrameViewModel()
+ }
+
if (viewModel.isEditMode && dragDropState != null) {
val isItemDragging = dragDropState.draggingItemKey == item.key
val outlineAlpha by
@@ -821,6 +833,7 @@
)
}
.thenIf(isItemDragging) { Modifier.zIndex(1f) },
+ viewModel = resizeableItemFrameViewModel,
onResize = { resizeInfo -> contentListState.resize(index, resizeInfo) },
minHeightPx = widgetSizeInfo.minHeightPx,
maxHeightPx = widgetSizeInfo.maxHeightPx,
@@ -843,6 +856,7 @@
contentListState = contentListState,
interactionHandler = interactionHandler,
widgetSection = widgetSection,
+ resizeableItemFrameViewModel = resizeableItemFrameViewModel,
)
}
}
@@ -857,6 +871,7 @@
contentListState = contentListState,
interactionHandler = interactionHandler,
widgetSection = widgetSection,
+ resizeableItemFrameViewModel = resizeableItemFrameViewModel,
)
}
}
@@ -1080,6 +1095,7 @@
contentListState: ContentListState,
interactionHandler: RemoteViews.InteractionHandler?,
widgetSection: CommunalAppWidgetSection,
+ resizeableItemFrameViewModel: ResizeableItemFrameViewModel,
) {
when (model) {
is CommunalContentModel.WidgetContent.Widget ->
@@ -1093,6 +1109,7 @@
index,
contentListState,
widgetSection,
+ resizeableItemFrameViewModel,
)
is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
is CommunalContentModel.WidgetContent.DisabledWidget ->
@@ -1223,7 +1240,9 @@
index: Int,
contentListState: ContentListState,
widgetSection: CommunalAppWidgetSection,
+ resizeableItemFrameViewModel: ResizeableItemFrameViewModel,
) {
+ val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val accessibilityLabel =
remember(model, context) {
@@ -1234,6 +1253,10 @@
val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget)
val unselectWidgetActionLabel =
stringResource(R.string.accessibility_action_label_unselect_widget)
+
+ val shrinkWidgetLabel = stringResource(R.string.accessibility_action_label_shrink_widget)
+ val expandWidgetLabel = stringResource(R.string.accessibility_action_label_expand_widget)
+
val selectedKey by viewModel.selectedKey.collectAsStateWithLifecycle()
val selectedIndex =
selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
@@ -1292,6 +1315,29 @@
true
}
val actions = mutableListOf(deleteAction)
+
+ if (communalWidgetResizing() && resizeableItemFrameViewModel.canShrink()) {
+ actions.add(
+ CustomAccessibilityAction(shrinkWidgetLabel) {
+ coroutineScope.launch {
+ resizeableItemFrameViewModel.shrinkToNextAnchor()
+ }
+ true
+ }
+ )
+ }
+
+ if (communalWidgetResizing() && resizeableItemFrameViewModel.canExpand()) {
+ actions.add(
+ CustomAccessibilityAction(expandWidgetLabel) {
+ coroutineScope.launch {
+ resizeableItemFrameViewModel.expandToNextAnchor()
+ }
+ true
+ }
+ )
+ }
+
if (selectedIndex != null && selectedIndex != index) {
actions.add(
CustomAccessibilityAction(placeWidgetActionLabel) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
index 521330f..8e85432 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
@@ -56,7 +56,6 @@
import com.android.systemui.communal.ui.viewmodel.DragHandle
import com.android.systemui.communal.ui.viewmodel.ResizeInfo
import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
-import com.android.systemui.lifecycle.rememberViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@@ -192,16 +191,12 @@
maxHeightPx: Int = Int.MAX_VALUE,
resizeMultiple: Int = 1,
alpha: () -> Float = { 1f },
+ viewModel: ResizeableItemFrameViewModel,
onResize: (info: ResizeInfo) -> Unit = {},
content: @Composable () -> Unit,
) {
val brush = SolidColor(outlineColor)
val onResizeUpdated by rememberUpdatedState(onResize)
- val viewModel =
- rememberViewModel(key = currentSpan, traceName = "ResizeableItemFrame.viewModel") {
- ResizeableItemFrameViewModel()
- }
-
val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
val isDragging by
remember(viewModel) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 58801e0..e725ce5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -128,10 +128,10 @@
QSSceneAdapter.State.QS
}
else ->
- error(
- "Bad transition for QuickSettings: fromContent=$fromContent," +
- " toScene=$toContent"
- )
+ // We are not in a transition between states that have QS, so just make
+ // sure it's closed. This could be an issue if going from SplitShade to
+ // a folded device.
+ QSSceneAdapter.State.CLOSED
}
}
is TransitionState.Transition.OverlayTransition ->
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 491221f..bba3d69 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -516,13 +516,14 @@
.weight(1f)
.graphicsLayer { translationX = unfoldTranslationXForStartSide }
) {
- BrightnessMirror(
- viewModel = brightnessMirrorViewModel,
- qsSceneAdapter = viewModel.qsSceneAdapter,
- // Need to use the offset measured from the container as the header
- // has to be accounted for
- measureFromContainer = true,
- )
+ Box(modifier = Modifier.fillMaxSize()) {
+ BrightnessMirror(
+ viewModel = brightnessMirrorViewModel,
+ qsSceneAdapter = viewModel.qsSceneAdapter,
+ modifier = Modifier.align(Alignment.TopCenter),
+ measureFromContainer = true,
+ )
+ }
Column(
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().padding(bottom = bottomPadding),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
index 58c3fec..bd33e52 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
@@ -20,7 +20,9 @@
import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -29,11 +31,12 @@
import android.graphics.Rect;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.VelocityTracker;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
@@ -42,9 +45,12 @@
import com.android.systemui.ambient.touch.scrim.ScrimController;
import com.android.systemui.ambient.touch.scrim.ScrimManager;
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
+import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -58,10 +64,14 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.List;
import java.util.Optional;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@DisableFlags(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN)
public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase {
@@ -114,6 +124,11 @@
@Mock
KeyguardInteractor mKeyguardInteractor;
+ @Mock
+ WindowRootView mWindowRootView;
+
+ private SceneInteractor mSceneInteractor;
+
private static final float TOUCH_REGION = .3f;
private static final float MIN_BOUNCER_HEIGHT = .05f;
@@ -124,9 +139,21 @@
/* flags= */ 0
);
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
+ }
+
+ public BouncerFullscreenSwipeTouchHandlerTest(FlagsParameterization flags) {
+ super();
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
@Before
public void setup() {
mKosmos = new KosmosJavaAdapter(this);
+ mSceneInteractor = spy(mKosmos.getSceneInteractor());
+
MockitoAnnotations.initMocks(this);
mTouchHandler = new BouncerSwipeTouchHandler(
mKosmos.getTestScope(),
@@ -142,7 +169,9 @@
MIN_BOUNCER_HEIGHT,
mUiEventLogger,
mActivityStarter,
- mKeyguardInteractor);
+ mKeyguardInteractor,
+ mSceneInteractor,
+ Optional.of(() -> mWindowRootView));
when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -153,6 +182,38 @@
}
/**
+ * Makes sure that touches go to the scene container when the flag is on.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+ public void testSwipeUp_sendsTouchesToWindowRootView() {
+ mTouchHandler.onGlanceableTouchAvailable(true);
+ mTouchHandler.onSessionStart(mTouchSession);
+ ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+ verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+ final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
+
+ final int screenHeight = 100;
+ final float distanceY = screenHeight * 0.42f;
+
+ final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+ 0, screenHeight, 0);
+ final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+ 0, screenHeight - distanceY, 0);
+
+ assertThat(gestureListener.onScroll(event1, event2, 0,
+ distanceY))
+ .isTrue();
+
+ // Ensure only called once
+ verify(mSceneInteractor).onRemoteUserInputStarted(any());
+ verify(mWindowRootView).dispatchTouchEvent(event1);
+ verify(mWindowRootView).dispatchTouchEvent(event2);
+ }
+
+ /**
* Ensures expansion does not happen for full vertical swipes when touch is not available.
*/
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 9568167..494e0b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -26,6 +26,7 @@
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -37,12 +38,12 @@
import android.graphics.Region;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.VelocityTracker;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
@@ -52,9 +53,12 @@
import com.android.systemui.ambient.touch.scrim.ScrimManager;
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel;
+import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -70,10 +74,14 @@
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import java.util.List;
import java.util.Optional;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
private KosmosJavaAdapter mKosmos;
@@ -122,6 +130,9 @@
Region mRegion;
@Mock
+ WindowRootView mWindowRootView;
+
+ @Mock
CommunalViewModel mCommunalViewModel;
@Mock
@@ -130,6 +141,8 @@
@Captor
ArgumentCaptor<Rect> mRectCaptor;
+ private SceneInteractor mSceneInteractor;
+
private static final float TOUCH_REGION = .3f;
private static final int SCREEN_WIDTH_PX = 1024;
private static final int SCREEN_HEIGHT_PX = 100;
@@ -142,9 +155,21 @@
/* flags= */ 0
);
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
+ }
+
+ public BouncerSwipeTouchHandlerTest(FlagsParameterization flags) {
+ super();
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
@Before
public void setup() {
mKosmos = new KosmosJavaAdapter(this);
+ mSceneInteractor = spy(mKosmos.getSceneInteractor());
+
MockitoAnnotations.initMocks(this);
mTouchHandler = new BouncerSwipeTouchHandler(
mKosmos.getTestScope(),
@@ -160,7 +185,10 @@
MIN_BOUNCER_HEIGHT,
mUiEventLogger,
mActivityStarter,
- mKeyguardInteractor);
+ mKeyguardInteractor,
+ mSceneInteractor,
+ Optional.of(() -> mWindowRootView)
+ );
when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
@@ -367,6 +395,7 @@
* Makes sure the expansion amount is proportional to (1 - scroll).
*/
@Test
+ @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
public void testSwipeUp_setsCorrectExpansionAmount() {
mTouchHandler.onSessionStart(mTouchSession);
ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
@@ -380,6 +409,36 @@
}
/**
+ * Makes sure that touches go to the scene container when the flag is on.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+ public void testSwipeUp_sendsTouchesToWindowRootView() {
+ mTouchHandler.onSessionStart(mTouchSession);
+ ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
+ ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+ verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
+
+ final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
+
+ final float distanceY = SCREEN_HEIGHT_PX * 0.42f;
+
+ final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+ 0, SCREEN_HEIGHT_PX, 0);
+ final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
+ 0, SCREEN_HEIGHT_PX - distanceY, 0);
+
+ assertThat(gestureListener.onScroll(event1, event2, 0,
+ distanceY))
+ .isTrue();
+
+ // Ensure only called once
+ verify(mSceneInteractor).onRemoteUserInputStarted(any());
+ verify(mWindowRootView).dispatchTouchEvent(event1);
+ verify(mWindowRootView).dispatchTouchEvent(event2);
+ }
+
+ /**
* Verifies that swiping up when the lock pattern is not secure dismissed dream and consumes
* the gesture.
*/
@@ -476,6 +535,7 @@
* Tests that ending an upward swipe before the set threshold leads to bouncer collapsing down.
*/
@Test
+ @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
public void testSwipeUpPositionBelowThreshold_collapsesBouncer() {
final float swipeUpPercentage = .3f;
final float expansion = 1 - swipeUpPercentage;
@@ -499,6 +559,7 @@
* Tests that ending an upward swipe above the set threshold will continue the expansion.
*/
@Test
+ @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
public void testSwipeUpPositionAboveThreshold_expandsBouncer() {
final float swipeUpPercentage = .7f;
final float expansion = 1 - swipeUpPercentage;
@@ -528,6 +589,7 @@
* Tests that swiping up with a speed above the set threshold will continue the expansion.
*/
@Test
+ @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
public void testSwipeUpVelocityAboveMin_expandsBouncer() {
when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
index 38ea4497..fa5af51 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
@@ -18,9 +18,9 @@
import android.app.DreamManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.view.GestureDetector
import android.view.MotionEvent
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
@@ -28,14 +28,20 @@
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.sceneContainerRepository
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shared.system.InputChannelCompat
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.testKosmos
import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import java.util.Optional
+import javax.inject.Provider
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,22 +53,29 @@
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class ShadeTouchHandlerTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class ShadeTouchHandlerTest(flags: FlagsParameterization) : SysuiTestCase() {
private var kosmos = testKosmos()
private var mCentralSurfaces = mock<CentralSurfaces>()
private var mShadeViewController = mock<ShadeViewController>()
private var mDreamManager = mock<DreamManager>()
private var mTouchSession = mock<TouchSession>()
private var communalViewModel = mock<CommunalViewModel>()
+ private var windowRootView = mock<WindowRootView>()
private lateinit var mTouchHandler: ShadeTouchHandler
private var mGestureListenerCaptor = argumentCaptor<GestureDetector.OnGestureListener>()
private var mInputListenerCaptor = argumentCaptor<InputChannelCompat.InputEventListener>()
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Before
fun setup() {
mTouchHandler =
@@ -73,7 +86,9 @@
mDreamManager,
communalViewModel,
kosmos.communalSettingsInteractor,
- TOUCH_HEIGHT
+ kosmos.sceneInteractor,
+ Optional.of(Provider<WindowRootView> { windowRootView }),
+ TOUCH_HEIGHT,
)
}
@@ -97,7 +112,7 @@
// Verifies that a swipe down forwards captured touches to central surfaces for handling.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
fun testSwipeDown_communalEnabled_sentToCentralSurfaces() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -110,7 +125,11 @@
// Verifies that a swipe down forwards captured touches to the shade view for handling.
@Test
- @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(
+ Flags.FLAG_COMMUNAL_HUB,
+ Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+ Flags.FLAG_SCENE_CONTAINER,
+ )
fun testSwipeDown_communalDisabled_sentToShadeView() {
swipe(Direction.DOWN)
@@ -121,7 +140,7 @@
// Verifies that a swipe down while dreaming forwards captured touches to the shade view for
// handling.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
fun testSwipeDown_dreaming_sentToShadeView() {
whenever(mDreamManager.isDreaming).thenReturn(true)
swipe(Direction.DOWN)
@@ -130,9 +149,39 @@
verify(mShadeViewController, times(2)).handleExternalTouch(any())
}
+ // Verifies that a swipe down forwards captured touches to the window root view for handling.
+ @Test
+ @EnableFlags(
+ Flags.FLAG_COMMUNAL_HUB,
+ Flags.FLAG_SCENE_CONTAINER,
+ Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+ )
+ fun testSwipeDown_sceneContainerEnabled_sentToWindowRootView() {
+ mTouchHandler.onGlanceableTouchAvailable(true)
+
+ swipe(Direction.DOWN)
+
+ // Both motion events are sent for central surfaces to process.
+ assertThat(kosmos.sceneContainerRepository.isRemoteUserInputOngoing.value).isTrue()
+ verify(windowRootView, times(2)).dispatchTouchEvent(any())
+ }
+
+ // Verifies that a swipe down while dreaming forwards captured touches to the window root view
+ // for handling.
+ @Test
+ @EnableFlags(Flags.FLAG_SCENE_CONTAINER)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ fun testSwipeDown_sceneContainerEnabledFullscreenSwipeDisabled_sentToWindowRootView() {
+ swipe(Direction.DOWN)
+
+ // Both motion events are sent for the shade view to process.
+ assertThat(kosmos.sceneContainerRepository.isRemoteUserInputOngoing.value).isTrue()
+ verify(windowRootView, times(2)).dispatchTouchEvent(any())
+ }
+
// Verifies that a swipe up is not forwarded to central surfaces.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER)
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
fun testSwipeUp_communalEnabled_touchesNotSent() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -146,7 +195,11 @@
// Verifies that a swipe up is not forwarded to the shade view.
@Test
- @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
+ @DisableFlags(
+ Flags.FLAG_COMMUNAL_HUB,
+ Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX,
+ Flags.FLAG_SCENE_CONTAINER,
+ )
fun testSwipeUp_communalDisabled_touchesNotSent() {
swipe(Direction.UP)
@@ -155,6 +208,17 @@
verify(mShadeViewController, never()).handleExternalTouch(any())
}
+ // Verifies that a swipe up is not forwarded to the window root view.
+ @Test
+ @EnableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_SCENE_CONTAINER)
+ fun testSwipeUp_sceneContainerEnabled_touchesNotSent() {
+ swipe(Direction.UP)
+
+ // Motion events are not sent for window root view to process as the swipe is going in the
+ // wrong direction.
+ verify(windowRootView, never()).dispatchTouchEvent(any())
+ }
+
@Test
@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testCancelMotionEvent_popsTouchSession() {
@@ -243,10 +307,16 @@
private enum class Direction {
DOWN,
- UP
+ UP,
}
companion object {
private const val TOUCH_HEIGHT = 20
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
index 22b114c..0269577 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
@@ -366,6 +366,106 @@
assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
}
+ @Test
+ fun testCanExpand_atTopPosition_withMultipleAnchors_returnsTrue() =
+ testScope.runTest {
+ val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0)
+
+ updateGridLayout(twoRowGrid)
+ assertThat(underTest.canExpand()).isTrue()
+ assertThat(underTest.bottomDragState.anchors.toList())
+ .containsAtLeast(0 to 0f, 1 to 45f)
+ }
+
+ @Test
+ fun testCanExpand_atTopPosition_withSingleAnchors_returnsFalse() =
+ testScope.runTest {
+ val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0)
+ updateGridLayout(oneRowGrid)
+ assertThat(underTest.canExpand()).isFalse()
+ }
+
+ @Test
+ fun testCanExpand_atBottomPosition_withMultipleAnchors_returnsTrue() =
+ testScope.runTest {
+ val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1)
+ updateGridLayout(twoRowGrid)
+ assertThat(underTest.canExpand()).isTrue()
+ assertThat(underTest.topDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f)
+ }
+
+ @Test
+ fun testCanShrink_atMinimumHeight_returnsFalse() =
+ testScope.runTest {
+ val oneRowGrid = singleSpanGrid.copy(totalSpans = 1, currentSpan = 1, currentRow = 0)
+ updateGridLayout(oneRowGrid)
+ assertThat(underTest.canShrink()).isFalse()
+ }
+
+ @Test
+ fun testCanShrink_atFullSize_checksBottomDragState() = runTestWithSnapshots {
+ val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0)
+ updateGridLayout(twoSpanGrid)
+
+ assertThat(underTest.canShrink()).isTrue()
+ assertThat(underTest.bottomDragState.anchors.toList()).containsAtLeast(0 to 0f, -1 to -45f)
+ }
+
+ @Test
+ fun testResizeByAccessibility_expandFromBottom_usesTopDragState() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+ val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 1)
+ updateGridLayout(twoSpanGrid)
+
+ underTest.expandToNextAnchor()
+
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.TOP))
+ }
+
+ @Test
+ fun testResizeByAccessibility_expandFromTop_usesBottomDragState() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+ val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0)
+ updateGridLayout(twoSpanGrid)
+
+ underTest.expandToNextAnchor()
+
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.BOTTOM))
+ }
+
+ @Test
+ fun testResizeByAccessibility_shrinkFromFull_usesBottomDragState() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+ val twoSpanGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 2, currentRow = 0)
+ updateGridLayout(twoSpanGrid)
+
+ underTest.shrinkToNextAnchor()
+
+ assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM))
+ }
+
+ @Test
+ fun testResizeByAccessibility_cannotResizeAtMinSize() = runTestWithSnapshots {
+ val resizeInfo by collectLastValue(underTest.resizeInfo)
+
+ // Set up grid at minimum size
+ val minSizeGrid =
+ singleSpanGrid.copy(
+ totalSpans = 2,
+ currentSpan = 1,
+ minHeightPx = singleSpanGrid.minHeightPx,
+ currentRow = 0,
+ )
+ updateGridLayout(minSizeGrid)
+
+ underTest.shrinkToNextAnchor()
+
+ assertThat(resizeInfo).isNull()
+ }
+
@Test(expected = IllegalArgumentException::class)
fun testIllegalState_maxHeightLessThanMinHeight() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index 83d2617..32fa160 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -20,6 +20,7 @@
import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.os.UserHandle
+import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
@@ -81,6 +82,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@DisableSceneContainer
+@FlakyTest(bugId = 292574995, detail = "NullPointer on MockMakerTypeMockability.mockable()")
class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index 527aeaa..83f95ea 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -28,6 +28,8 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
@@ -604,6 +606,46 @@
assertThat(mPagedTileLayoutListening).isFalse();
}
+ @Test
+ public void reAttach_configurationChangePending_triggersConfigurationListener() {
+ mController.onViewDetached();
+
+ when(mQSPanel.hadConfigurationChangeWhileDetached()).thenReturn(true);
+ clearInvocations(mQSLogger);
+
+ mController.onViewAttached();
+
+ verify(mQSLogger).logOnConfigurationChanged(
+ anyInt(),
+ anyInt(),
+ anyBoolean(),
+ anyBoolean(),
+ anyInt(),
+ anyInt(),
+ anyString()
+ );
+ }
+
+ @Test
+ public void reAttach_noConfigurationChangePending_doesntTriggerConfigurationListener() {
+ mController.onViewDetached();
+
+ when(mQSPanel.hadConfigurationChangeWhileDetached()).thenReturn(false);
+ clearInvocations(mQSLogger);
+
+ mController.onViewAttached();
+
+ verify(mQSLogger, never()).logOnConfigurationChanged(
+ anyInt(),
+ anyInt(),
+ anyBoolean(),
+ anyBoolean(),
+ anyInt(),
+ anyInt(),
+ anyString()
+ );
+ }
+
private boolean usingMediaPlayer() {
return !SceneContainerFlag.isEnabled();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelTest.kt
similarity index 74%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelTest.kt
index 118dea6..69a7627 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelTest.kt
@@ -14,17 +14,17 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel
+package com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel
import android.content.packageManager
import android.graphics.drawable.BitmapDrawable
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -40,13 +40,13 @@
import org.mockito.kotlin.whenever
@SmallTest
-class DemoRonChipViewModelTest : SysuiTestCase() {
+class DemoNotifChipViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val commandRegistry = kosmos.commandRegistry
private val pw = PrintWriter(StringWriter())
- private val underTest = kosmos.demoRonChipViewModel
+ private val underTest = kosmos.demoNotifChipViewModel
@Before
fun setUp() {
@@ -56,61 +56,61 @@
}
@Test
- @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+ @DisableFlags(StatusBarNotifChips.FLAG_NAME)
fun chip_flagOff_hidden() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
- addDemoRonChip()
+ addDemoNotifChip()
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
fun chip_noPackage_hidden() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
- commandRegistry.onShellCommand(pw, arrayOf("demo-ron"))
+ commandRegistry.onShellCommand(pw, arrayOf("demo-notif"))
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
fun chip_hasPackage_shown() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
- commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "-p", "com.android.systemui"))
+ commandRegistry.onShellCommand(pw, arrayOf("demo-notif", "-p", "com.android.systemui"))
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
fun chip_hasText_shownWithText() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
commandRegistry.onShellCommand(
pw,
- arrayOf("demo-ron", "-p", "com.android.systemui", "-t", "test")
+ arrayOf("demo-notif", "-p", "com.android.systemui", "-t", "test"),
)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Text::class.java)
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
fun chip_supportsColor() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
commandRegistry.onShellCommand(
pw,
- arrayOf("demo-ron", "-p", "com.android.systemui", "-c", "#434343")
+ arrayOf("demo-notif", "-p", "com.android.systemui", "-c", "#434343"),
)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
@@ -119,28 +119,28 @@
}
@Test
- @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME)
fun chip_hasHideArg_hidden() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
// First, show a chip
- addDemoRonChip()
+ addDemoNotifChip()
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
// Then, hide the chip
- commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "--hide"))
+ commandRegistry.onShellCommand(pw, arrayOf("demo-notif", "--hide"))
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
- private fun addDemoRonChip() {
- Companion.addDemoRonChip(commandRegistry, pw)
+ private fun addDemoNotifChip() {
+ addDemoNotifChip(commandRegistry, pw)
}
companion object {
- fun addDemoRonChip(commandRegistry: CommandRegistry, pw: PrintWriter) {
- commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "-p", "com.android.systemui"))
+ fun addDemoNotifChip(commandRegistry: CommandRegistry, pw: PrintWriter) {
+ commandRegistry.onShellCommand(pw, arrayOf("demo-notif", "-p", "com.android.systemui"))
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 2872900..eb5d931 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -19,12 +19,11 @@
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.chips.ron.ui.viewmodel.notifChipsViewModel
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
@@ -42,7 +41,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
-@EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+@EnableFlags(StatusBarNotifChips.FLAG_NAME)
class NotifChipsViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 26ce7b9..e96def6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -25,7 +25,6 @@
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
@@ -40,7 +39,8 @@
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.demoNotifChipViewModel
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -66,13 +66,11 @@
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-/**
- * Tests for [OngoingActivityChipsViewModel] when the [FLAG_STATUS_BAR_RON_CHIPS] flag is disabled.
- */
+/** Tests for [OngoingActivityChipsViewModel] when the [StatusBarNotifChips] flag is disabled. */
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
-@DisableFlags(FLAG_STATUS_BAR_RON_CHIPS)
+@DisableFlags(StatusBarNotifChips.FLAG_NAME)
class OngoingActivityChipsViewModelTest : SysuiTestCase() {
private val kosmos = Kosmos().also { it.testCase = this }
private val testScope = kosmos.testScope
@@ -99,11 +97,11 @@
@Before
fun setUp() {
setUpPackageManagerForMediaProjection(kosmos)
- kosmos.demoRonChipViewModel.start()
+ kosmos.demoNotifChipViewModel.start()
val icon =
BitmapDrawable(
context.resources,
- Bitmap.createBitmap(/* width= */ 100, /* height= */ 100, Bitmap.Config.ARGB_8888)
+ Bitmap.createBitmap(/* width= */ 100, /* height= */ 100, Bitmap.Config.ARGB_8888),
)
whenever(kosmos.packageManager.getApplicationIcon(any<String>())).thenReturn(icon)
}
@@ -325,7 +323,7 @@
latest: OngoingActivityChipModel?,
chipView: View,
dialog: SystemUIDialog,
- kosmos: Kosmos
+ kosmos: Kosmos,
): DialogInterface.OnClickListener {
// Capture the action that would get invoked when the user clicks "Stop" on the dialog
lateinit var dialogStopAction: DialogInterface.OnClickListener
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
similarity index 93%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index c5b857f..b12d7c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -24,7 +24,6 @@
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
@@ -37,9 +36,10 @@
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModelTest.Companion.addDemoNotifChip
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.demoNotifChipViewModel
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModelTest.Companion.addDemoRonChip
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
@@ -73,14 +73,12 @@
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
-/**
- * Tests for [OngoingActivityChipsViewModel] when the [FLAG_STATUS_BAR_RON_CHIPS] flag is enabled.
- */
+/** Tests for [OngoingActivityChipsViewModel] when the [StatusBarNotifChips] flag is enabled. */
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
-@EnableFlags(FLAG_STATUS_BAR_RON_CHIPS)
-class OngoingActivityChipsWithRonsViewModelTest : SysuiTestCase() {
+@EnableFlags(StatusBarNotifChips.FLAG_NAME)
+class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val systemClock = kosmos.fakeSystemClock
@@ -110,7 +108,7 @@
@Before
fun setUp() {
setUpPackageManagerForMediaProjection(kosmos)
- kosmos.demoRonChipViewModel.start()
+ kosmos.demoNotifChipViewModel.start()
val icon =
BitmapDrawable(
context.resources,
@@ -119,7 +117,7 @@
whenever(kosmos.packageManager.getApplicationIcon(any<String>())).thenReturn(icon)
}
- // Even though the `primaryChip` flow isn't used when the RONs flag is on, still test that the
+ // Even though the `primaryChip` flow isn't used when the notifs flag is on, still test that the
// flow has the right behavior to verify that we don't break any existing functionality.
@Test
@@ -256,13 +254,13 @@
testScope.runTest {
screenRecordState.value = ScreenRecordModel.Recording
callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
- addDemoRonChip(commandRegistry, pw)
+ addDemoNotifChip(commandRegistry, pw)
val latest by collectLastValue(underTest.chips)
assertIsScreenRecordChip(latest!!.primary)
assertIsCallChip(latest!!.secondary)
- // Demo RON chip is dropped
+ // Demo notif chip is dropped
}
@Test
@@ -389,7 +387,7 @@
fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
testScope.runTest {
// Start with just the lowest priority chip shown
- addDemoRonChip(commandRegistry, pw)
+ addDemoNotifChip(commandRegistry, pw)
// And everything else hidden
callRepo.setOngoingCallState(OngoingCallModel.NoCall)
mediaProjectionState.value = MediaProjectionState.NotProjecting
@@ -397,7 +395,7 @@
val latest by collectLastValue(underTest.primaryChip)
- assertIsDemoRonChip(latest)
+ assertIsDemoNotifChip(latest)
// WHEN the higher priority call chip is added
callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
@@ -431,7 +429,7 @@
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
- addDemoRonChip(commandRegistry, pw)
+ addDemoNotifChip(commandRegistry, pw)
val latest by collectLastValue(underTest.primaryChip)
@@ -453,15 +451,15 @@
// WHEN the higher priority call is removed
callRepo.setOngoingCallState(OngoingCallModel.NoCall)
- // THEN the lower priority demo RON is used
- assertIsDemoRonChip(latest)
+ // THEN the lower priority demo notif is used
+ assertIsDemoNotifChip(latest)
}
@Test
fun chips_movesChipsAroundAccordingToPriority() =
testScope.runTest {
// Start with just the lowest priority chip shown
- addDemoRonChip(commandRegistry, pw)
+ addDemoNotifChip(commandRegistry, pw)
// And everything else hidden
callRepo.setOngoingCallState(OngoingCallModel.NoCall)
mediaProjectionState.value = MediaProjectionState.NotProjecting
@@ -469,16 +467,16 @@
val latest by collectLastValue(underTest.chips)
- assertIsDemoRonChip(latest!!.primary)
+ assertIsDemoNotifChip(latest!!.primary)
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
// WHEN the higher priority call chip is added
callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
- // THEN the higher priority call chip is used as primary and demo ron is demoted to
+ // THEN the higher priority call chip is used as primary and demo notif is demoted to
// secondary
assertIsCallChip(latest!!.primary)
- assertIsDemoRonChip(latest!!.secondary)
+ assertIsDemoNotifChip(latest!!.secondary)
// WHEN the higher priority media projection chip is added
mediaProjectionState.value =
@@ -489,7 +487,7 @@
)
// THEN the higher priority media projection chip is used as primary and call is demoted
- // to secondary (and demo RON is dropped altogether)
+ // to secondary (and demo notif is dropped altogether)
assertIsShareToAppChip(latest!!.primary)
assertIsCallChip(latest!!.secondary)
@@ -503,15 +501,15 @@
screenRecordState.value = ScreenRecordModel.DoingNothing
callRepo.setOngoingCallState(OngoingCallModel.NoCall)
- // THEN media projection and demo RON remain
+ // THEN media projection and demo notif remain
assertIsShareToAppChip(latest!!.primary)
- assertIsDemoRonChip(latest!!.secondary)
+ assertIsDemoNotifChip(latest!!.secondary)
// WHEN media projection is dropped
mediaProjectionState.value = MediaProjectionState.NotProjecting
- // THEN demo RON is promoted to primary
- assertIsDemoRonChip(latest!!.primary)
+ // THEN demo notif is promoted to primary
+ assertIsDemoNotifChip(latest!!.primary)
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@@ -624,7 +622,7 @@
assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden(shouldAnimate = false))
}
- private fun assertIsDemoRonChip(latest: OngoingActivityChipModel?) {
+ private fun assertIsDemoNotifChip(latest: OngoingActivityChipModel?) {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
assertThat((latest as OngoingActivityChipModel.Shown).icon)
.isInstanceOf(OngoingActivityChipModel.ChipIcon.FullColorAppIcon::class.java)
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0aa5ccf..c838180 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1311,6 +1311,10 @@
<string name="communal_widget_picker_description">Anyone can view widgets on your lock screen, even if your tablet\'s locked.</string>
<!-- Label for accessibility action to unselect a widget in edit mode. [CHAR LIMIT=NONE] -->
<string name="accessibility_action_label_unselect_widget">unselect widget</string>
+ <!-- Label for accessibility action to shrink a widget in edit mode. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_label_shrink_widget">Decrease height</string>
+ <!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_label_expand_widget">Increase height</string>
<!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
<string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
<!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
index 223a21d..e365b77 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
@@ -27,6 +27,7 @@
import android.view.MotionEvent
import android.view.VelocityTracker
import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.systemui.Flags
@@ -38,6 +39,9 @@
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -45,12 +49,12 @@
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
+import javax.inject.Provider
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Monitor for tracking touches on the DreamOverlay to bring up the bouncer. */
class BouncerSwipeTouchHandler
@@ -74,6 +78,8 @@
private val uiEventLogger: UiEventLogger,
private val activityStarter: ActivityStarter,
private val keyguardInteractor: KeyguardInteractor,
+ private val sceneInteractor: SceneInteractor,
+ private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
) : TouchHandler {
/** An interface for creating ValueAnimators. */
interface ValueAnimatorCreator {
@@ -100,6 +106,8 @@
currentScrimController = controller
}
+ private val windowRootView by lazy { windowRootViewProvider.get().get() }
+
/** Determines whether the touch handler should process touches in fullscreen swiping mode */
private var touchAvailable = false
@@ -109,7 +117,7 @@
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
- distanceY: Float
+ distanceY: Float,
): Boolean {
if (capture == null) {
capture =
@@ -128,6 +136,11 @@
expanded = false
// Since the user is dragging the bouncer up, set scrimmed to false.
currentScrimController?.show()
+
+ if (SceneContainerFlag.isEnabled) {
+ sceneInteractor.onRemoteUserInputStarted("bouncer touch handler")
+ e1?.apply { windowRootView.dispatchTouchEvent(e1) }
+ }
}
}
if (capture != true) {
@@ -152,20 +165,27 @@
/* cancelAction= */ null,
/* dismissShade= */ true,
/* afterKeyguardGone= */ true,
- /* deferred= */ false
+ /* deferred= */ false,
)
return true
}
- // For consistency, we adopt the expansion definition found in the
- // PanelViewController. In this case, expansion refers to the view above the
- // bouncer. As that view's expansion shrinks, the bouncer appears. The bouncer
- // is fully hidden at full expansion (1) and fully visible when fully collapsed
- // (0).
- touchSession?.apply {
- val screenTravelPercentage =
- (abs((this@outer.y - e2.y).toDouble()) / getBounds().height()).toFloat()
- setPanelExpansion(1 - screenTravelPercentage)
+ if (SceneContainerFlag.isEnabled) {
+ windowRootView.dispatchTouchEvent(e2)
+ } else {
+ // For consistency, we adopt the expansion definition found in the
+ // PanelViewController. In this case, expansion refers to the view above the
+ // bouncer. As that view's expansion shrinks, the bouncer appears. The
+ // bouncer
+ // is fully hidden at full expansion (1) and fully visible when fully
+ // collapsed
+ // (0).
+ touchSession?.apply {
+ val screenTravelPercentage =
+ (abs((this@outer.y - e2.y).toDouble()) / getBounds().height())
+ .toFloat()
+ setPanelExpansion(1 - screenTravelPercentage)
+ }
}
}
@@ -194,7 +214,7 @@
ShadeExpansionChangeEvent(
/* fraction= */ currentExpansion,
/* expanded= */ expanded,
- /* tracking= */ true
+ /* tracking= */ true,
)
currentScrimController?.expand(event)
}
@@ -347,7 +367,7 @@
currentHeight,
targetHeight,
velocity,
- viewHeight
+ viewHeight,
)
} else {
// Shows the bouncer, i.e., fully collapses the space above the bouncer.
@@ -356,7 +376,7 @@
currentHeight,
targetHeight,
velocity,
- viewHeight
+ viewHeight,
)
}
animator.start()
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
index 1951a23..50e62a8 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
@@ -22,19 +22,23 @@
import android.view.InputEvent
import android.view.MotionEvent
import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.Flags
import com.android.systemui.ambient.touch.TouchHandler.TouchSession
import com.android.systemui.ambient.touch.dagger.ShadeModule
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.phone.CentralSurfaces
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
+import javax.inject.Provider
import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/**
* [ShadeTouchHandler] is responsible for handling swipe down gestures over dream to bring down the
@@ -49,8 +53,10 @@
private val dreamManager: DreamManager,
private val communalViewModel: CommunalViewModel,
private val communalSettingsInteractor: CommunalSettingsInteractor,
+ private val sceneInteractor: SceneInteractor,
+ private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
@param:Named(ShadeModule.NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT)
- private val initiationHeight: Int
+ private val initiationHeight: Int,
) : TouchHandler {
/**
* Tracks whether or not we are capturing a given touch. Will be null before and after a touch.
@@ -60,6 +66,8 @@
/** Determines whether the touch handler should process touches in fullscreen swiping mode */
private var touchAvailable = false
+ private val windowRootView by lazy { windowRootViewProvider.get().get() }
+
init {
if (Flags.hubmodeFullscreenVerticalSwipeFix()) {
scope.launch {
@@ -100,7 +108,7 @@
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
- distanceY: Float
+ distanceY: Float,
): Boolean {
if (capture == null) {
// Only capture swipes that are going downwards.
@@ -110,6 +118,10 @@
if (Flags.hubmodeFullscreenVerticalSwipeFix()) touchAvailable
else true
if (capture == true) {
+ if (SceneContainerFlag.isEnabled) {
+ sceneInteractor.onRemoteUserInputStarted("shade touch handler")
+ }
+
// Send the initial touches over, as the input listener has already
// processed these touches.
e1?.apply { sendTouchEvent(this) }
@@ -123,7 +135,7 @@
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
- velocityY: Float
+ velocityY: Float,
): Boolean {
return capture == true
}
@@ -132,6 +144,11 @@
}
private fun sendTouchEvent(event: MotionEvent) {
+ if (SceneContainerFlag.isEnabled) {
+ windowRootView.dispatchTouchEvent(event)
+ return
+ }
+
if (communalSettingsInteractor.isCommunalFlagEnabled() && !dreamManager.isDreaming) {
// Send touches to central surfaces only when on the glanceable hub while not dreaming.
// While sending touches where while dreaming will open the shade, the shade
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
index bc2f354..1c781d6 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/dagger/ShadeModule.java
@@ -22,8 +22,10 @@
import com.android.systemui.ambient.touch.TouchHandler;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.res.R;
+import com.android.systemui.scene.ui.view.WindowRootView;
import dagger.Binds;
+import dagger.BindsOptionalOf;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
@@ -51,6 +53,13 @@
ShadeTouchHandler touchHandler);
/**
+ * Window root view is used to send touches to the scene container. Declaring as optional as it
+ * may not be present on all SysUI variants.
+ */
+ @BindsOptionalOf
+ abstract WindowRootView bindWindowRootView();
+
+ /**
* Provides the height of the gesture area for notification swipe down.
*/
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
index db4bee7..bde5d0f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
@@ -17,6 +17,7 @@
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.foundation.gestures.snapTo
import androidx.compose.runtime.snapshotFlow
import com.android.app.tracing.coroutines.coroutineScopeTraced as coroutineScope
import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -81,6 +82,72 @@
get() = roundDownToMultiple(getSpansForPx(minHeightPx))
}
+ /** Check if widget can expanded based on current drag states */
+ fun canExpand(): Boolean {
+ return getNextAnchor(bottomDragState, moveUp = false) != null ||
+ getNextAnchor(topDragState, moveUp = true) != null
+ }
+
+ /** Check if widget can shrink based on current drag states */
+ fun canShrink(): Boolean {
+ return getNextAnchor(bottomDragState, moveUp = true) != null ||
+ getNextAnchor(topDragState, moveUp = false) != null
+ }
+
+ /** Get the next anchor value in the specified direction */
+ private fun getNextAnchor(state: AnchoredDraggableState<Int>, moveUp: Boolean): Int? {
+ var nextAnchor: Int? = null
+ var nextAnchorDiff = Int.MAX_VALUE
+ val currentValue = state.currentValue
+
+ for (i in 0 until state.anchors.size) {
+ val anchor = state.anchors.anchorAt(i) ?: continue
+ if (anchor == currentValue) continue
+
+ val diff =
+ if (moveUp) {
+ currentValue - anchor
+ } else {
+ anchor - currentValue
+ }
+
+ if (diff in 1..<nextAnchorDiff) {
+ nextAnchor = anchor
+ nextAnchorDiff = diff
+ }
+ }
+
+ return nextAnchor
+ }
+
+ /** Handle expansion to the next anchor */
+ suspend fun expandToNextAnchor() {
+ if (!canExpand()) return
+ val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = false)
+ if (bottomAnchor != null) {
+ bottomDragState.snapTo(bottomAnchor)
+ return
+ }
+ val topAnchor =
+ getNextAnchor(
+ state = topDragState,
+ moveUp = true, // Moving up to expand
+ )
+ topAnchor?.let { topDragState.snapTo(it) }
+ }
+
+ /** Handle shrinking to the next anchor */
+ suspend fun shrinkToNextAnchor() {
+ if (!canShrink()) return
+ val topAnchor = getNextAnchor(state = topDragState, moveUp = false)
+ if (topAnchor != null) {
+ topDragState.snapTo(topAnchor)
+ return
+ }
+ val bottomAnchor = getNextAnchor(state = bottomDragState, moveUp = true)
+ bottomAnchor?.let { bottomDragState.snapTo(it) }
+ }
+
/**
* The layout information necessary in order to calculate the pixel offsets of the drag anchor
* points.
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index c6be0dd..b966ad4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -35,6 +35,7 @@
import com.android.systemui.dock.DockManagerImpl;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.education.dagger.ContextualEducationModule;
+import com.android.systemui.emergency.EmergencyGestureModule;
import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule;
import com.android.systemui.keyboard.shortcut.ShortcutHelperModule;
import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule;
@@ -123,6 +124,7 @@
CollapsedStatusBarFragmentStartableModule.class,
ConnectingDisplayViewModel.StartableModule.class,
DefaultBlueprintModule.class,
+ EmergencyGestureModule.class,
GestureModule.class,
HeadsUpModule.class,
KeyboardShortcutsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 7a6ca08..1ffbbd2 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -65,7 +65,6 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
-import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -503,10 +502,10 @@
mDreamOverlayContainerViewController =
dreamOverlayComponent.getDreamOverlayContainerViewController();
- if (!SceneContainerFlag.isEnabled()) {
- mTouchMonitor = ambientTouchComponent.getTouchMonitor();
- mTouchMonitor.init();
- }
+ // Touch monitor are also used with SceneContainer. See individual touch handlers for
+ // handling of SceneContainer.
+ mTouchMonitor = ambientTouchComponent.getTouchMonitor();
+ mTouchMonitor.init();
mStateController.setShouldShowComplications(shouldShowComplications());
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index 12984efb..85fb90d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -30,8 +30,10 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayContainerView;
import com.android.systemui.res.R;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.touch.TouchInsetManager;
+import dagger.BindsOptionalOf;
import dagger.Module;
import dagger.Provides;
@@ -54,6 +56,13 @@
public static final String DREAM_IN_TRANSLATION_Y_DURATION =
"dream_in_complications_translation_y_duration";
+ /**
+ * Window root view is used to send touches to the scene container. Declaring as optional as it
+ * may not be present on all SysUI variants.
+ */
+ @BindsOptionalOf
+ abstract WindowRootView bindWindowRootView();
+
/** */
@Provides
@DreamOverlayComponent.DreamOverlayScope
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
index 5ba780f..42a6877 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -31,6 +31,9 @@
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
import com.android.systemui.communal.domain.interactor.CommunalInteractor;
import com.android.systemui.dreams.touch.dagger.CommunalTouchModule;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
+import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import kotlinx.coroutines.Job;
@@ -42,6 +45,7 @@
import javax.inject.Inject;
import javax.inject.Named;
+import javax.inject.Provider;
/** {@link TouchHandler} responsible for handling touches to open communal hub. **/
public class CommunalTouchHandler implements TouchHandler {
@@ -51,6 +55,8 @@
private final CommunalInteractor mCommunalInteractor;
private final ConfigurationInteractor mConfigurationInteractor;
+ private final SceneInteractor mSceneInteractor;
+ private final WindowRootView mWindowRootView;
private Boolean mIsEnabled = false;
private ArrayList<Job> mFlows = new ArrayList<>();
@@ -69,12 +75,16 @@
@Named(CommunalTouchModule.COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth,
CommunalInteractor communalInteractor,
ConfigurationInteractor configurationInteractor,
+ SceneInteractor sceneInteractor,
+ Optional<Provider<WindowRootView>> windowRootViewProvider,
Lifecycle lifecycle) {
mInitiationWidth = initiationWidth;
mCentralSurfaces = centralSurfaces;
mLifecycle = lifecycle;
mCommunalInteractor = communalInteractor;
mConfigurationInteractor = configurationInteractor;
+ mSceneInteractor = sceneInteractor;
+ mWindowRootView = windowRootViewProvider.get().get();
mFlows.add(collectFlow(
mLifecycle,
@@ -125,8 +135,15 @@
private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) {
// Notification shade window has its own logic to be visible if the hub is open, no need to
// do anything here other than send touch events over.
+ if (SceneContainerFlag.isEnabled()) {
+ mSceneInteractor.onRemoteUserInputStarted("communal touch handler");
+ }
session.registerInputListener(ev -> {
- surfaces.handleCommunalHubTouch((MotionEvent) ev);
+ if (SceneContainerFlag.isEnabled()) {
+ mWindowRootView.dispatchTouchEvent((MotionEvent) ev);
+ } else {
+ surfaces.handleCommunalHubTouch((MotionEvent) ev);
+ }
if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
var unused = session.pop();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index d3bed27..602f593 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -116,6 +116,8 @@
@Nullable
private View mMediaViewPlaceHolderForScene;
+ private boolean mHadConfigurationChangeWhileDetached;
+
public QSPanel(Context context, AttributeSet attrs) {
super(context, attrs);
mUsingMediaPlayer = useQsMediaPlayer(context);
@@ -425,10 +427,23 @@
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
+ if (!isAttachedToWindow()) {
+ mHadConfigurationChangeWhileDetached = true;
+ }
mOnConfigurationChangedListeners.forEach(
listener -> listener.onConfigurationChange(newConfig));
}
+ final boolean hadConfigurationChangeWhileDetached() {
+ return mHadConfigurationChangeWhileDetached;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mHadConfigurationChangeWhileDetached = false;
+ }
+
@Override
protected void onFinishInflate() {
super.onFinishInflate();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 6cf5b32..85bcc25 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -234,6 +234,12 @@
mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener);
}
mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
+ // We were not attached and the configuration may have changed, trigger the listener.
+ if (mView.hadConfigurationChangeWhileDetached()) {
+ mOnConfigurationChangedListener.onConfigurationChange(
+ getContext().getResources().getConfiguration()
+ );
+ }
setTiles();
mLastOrientation = getResources().getConfiguration().orientation;
mLastScreenLayout = getResources().getConfiguration().screenLayout;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
index be733d4..8ce0dbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt
@@ -20,7 +20,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModel
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -31,8 +31,8 @@
abstract class StatusBarChipsModule {
@Binds
@IntoMap
- @ClassKey(DemoRonChipViewModel::class)
- abstract fun binds(impl: DemoRonChipViewModel): CoreStartable
+ @ClassKey(DemoNotifChipViewModel::class)
+ abstract fun binds(impl: DemoNotifChipViewModel): CoreStartable
companion object {
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModel.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModel.kt
index cce9a16..5fa19dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModel.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel
+package com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
@@ -22,7 +22,7 @@
import com.android.systemui.CoreStartable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
@@ -37,25 +37,25 @@
import kotlinx.coroutines.flow.asStateFlow
/**
- * A view model that will emit demo RON chips (rich ongoing notification chips) from [chip] based on
- * adb commands sent by the user.
+ * A view model that will emit demo promoted ongoing notification chips from [chip] based on adb
+ * commands sent by the user.
*
* Example adb commands:
*
* To show a chip with the SysUI icon and custom text and color:
* ```
- * adb shell cmd statusbar demo-ron -p com.android.systemui -t 10min -c "\\#434343"
+ * adb shell cmd statusbar demo-notif -p com.android.systemui -t 10min -c "\\#434343"
* ```
*
* To hide the chip:
* ```
- * adb shell cmd statusbar demo-ron --hide
+ * adb shell cmd statusbar demo-notif --hide
* ```
*
- * See [DemoRonCommand] for more information on the adb command spec.
+ * See [DemoNotifCommand] for more information on the adb command spec.
*/
@SysUISingleton
-class DemoRonChipViewModel
+class DemoNotifChipViewModel
@Inject
constructor(
private val commandRegistry: CommandRegistry,
@@ -63,19 +63,19 @@
private val systemClock: SystemClock,
) : OngoingActivityChipViewModel, CoreStartable {
override fun start() {
- commandRegistry.registerCommand("demo-ron") { DemoRonCommand() }
+ commandRegistry.registerCommand(DEMO_COMMAND_NAME) { DemoNotifCommand() }
}
private val _chip =
MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden())
override val chip: StateFlow<OngoingActivityChipModel> = _chip.asStateFlow()
- private inner class DemoRonCommand : ParseableCommand("demo-ron") {
+ private inner class DemoNotifCommand : ParseableCommand(DEMO_COMMAND_NAME) {
private val packageName: String? by
param(
longName = "packageName",
shortName = "p",
- description = "The package name for the demo RON app",
+ description = "The package name for app \"posting\" the demo notification",
valueParser = Type.String,
)
@@ -99,15 +99,12 @@
)
private val hide by
- flag(
- longName = "hide",
- description = "Hides any existing demo RON chip",
- )
+ flag(longName = "hide", description = "Hides any existing demo notification chip")
override fun execute(pw: PrintWriter) {
- if (!StatusBarRonChips.isEnabled) {
+ if (!StatusBarNotifChips.isEnabled) {
pw.println(
- "Error: com.android.systemui.status_bar_ron_chips must be enabled " +
+ "Error: com.android.systemui.status_bar_notification_chips must be enabled " +
"before using this demo feature"
)
return
@@ -167,8 +164,12 @@
return null
}
return OngoingActivityChipModel.ChipIcon.FullColorAppIcon(
- Icon.Loaded(drawable = iconDrawable, contentDescription = null),
+ Icon.Loaded(drawable = iconDrawable, contentDescription = null)
)
}
}
+
+ companion object {
+ private const val DEMO_COMMAND_NAME = "demo-notif"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
similarity index 87%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
index 4ef1909..47ffbaf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/shared/StatusBarNotifChips.kt
@@ -14,17 +14,17 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.chips.ron.shared
+package com.android.systemui.statusbar.chips.notification.shared
import com.android.systemui.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
-/** Helper for reading or using the status bar RON chips flag state. */
+/** Helper for reading or using the status bar promoted notification chips flag state. */
@Suppress("NOTHING_TO_INLINE")
-object StatusBarRonChips {
+object StatusBarNotifChips {
/** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_STATUS_BAR_RON_CHIPS
+ const val FLAG_NAME = Flags.FLAG_STATUS_BAR_NOTIFICATION_CHIPS
/** A token used for dependency declaration */
val token: FlagToken
@@ -33,7 +33,7 @@
/** Is the refactor enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.statusBarRonChips()
+ get() = Flags.statusBarNotificationChips()
/**
* Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 2220caab..f4462a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -28,7 +28,7 @@
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.chips.ui.view.ChipChronometer
@@ -102,7 +102,7 @@
defaultIconView.tintView(iconTint)
}
is OngoingActivityChipModel.ChipIcon.FullColorAppIcon -> {
- StatusBarRonChips.assertInNewMode()
+ StatusBarNotifChips.assertInNewMode()
IconViewBinder.bind(icon.impl, defaultIconView)
defaultIconView.visibility = View.VISIBLE
defaultIconView.untintView()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 2366572..cf07af1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -20,7 +20,7 @@
import com.android.systemui.Flags
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
/** Model representing the display of an ongoing activity as a chip in the status bar. */
sealed class OngoingActivityChipModel {
@@ -91,10 +91,7 @@
override val onClickListener: View.OnClickListener?,
) : Shown(icon, colors, onClickListener) {
init {
- check(StatusBarRonChips.isEnabled) {
- "OngoingActivityChipModel.Shown.ShortTimeDelta created even though " +
- "Flags.statusBarRonChips is not enabled"
- }
+ StatusBarNotifChips.assertInNewMode()
}
override val logName = "Shown.ShortTimeDelta"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 954386e..ed32597 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.chips.ui.viewmodel
-import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
@@ -24,9 +23,9 @@
import com.android.systemui.statusbar.chips.StatusBarChipsLog
import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel
import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModel
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModel
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModel
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel
import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel
@@ -57,7 +56,7 @@
castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel,
callChipViewModel: CallChipViewModel,
notifChipsViewModel: NotifChipsViewModel,
- demoRonChipViewModel: DemoRonChipViewModel,
+ demoNotifChipViewModel: DemoNotifChipViewModel,
@StatusBarChipsLog private val logger: LogBuffer,
) {
private enum class ChipType {
@@ -66,8 +65,8 @@
CastToOtherDevice,
Call,
Notification,
- /** A demo of a RON chip (rich ongoing notification chip), used just for testing. */
- DemoRon,
+ /** A demo of a notification chip, used just for testing. */
+ DemoNotification,
}
/** Model that helps us internally track the various chip states from each of the types. */
@@ -89,7 +88,7 @@
val castToOtherDevice: OngoingActivityChipModel.Hidden,
val call: OngoingActivityChipModel.Hidden,
val notifs: OngoingActivityChipModel.Hidden,
- val demoRon: OngoingActivityChipModel.Hidden,
+ val demoNotif: OngoingActivityChipModel.Hidden,
) : InternalChipModel
}
@@ -99,7 +98,7 @@
val castToOtherDevice: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
val call: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
val notifs: List<OngoingActivityChipModel.Shown> = emptyList(),
- val demoRon: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
+ val demoNotif: OngoingActivityChipModel = OngoingActivityChipModel.Hidden(),
)
/** Bundles all the incoming chips into one object to easily pass to various flows. */
@@ -110,8 +109,8 @@
castToOtherDeviceChipViewModel.chip,
callChipViewModel.chip,
notifChipsViewModel.chips,
- demoRonChipViewModel.chip,
- ) { screenRecord, shareToApp, castToOtherDevice, call, notifs, demoRon ->
+ demoNotifChipViewModel.chip,
+ ) { screenRecord, shareToApp, castToOtherDevice, call, notifs, demoNotif ->
logger.log(
TAG,
LogLevel.INFO,
@@ -129,9 +128,9 @@
str1 = call.logName
// TODO(b/364653005): Log other information for notification chips.
str2 = notifs.map { it.logName }.toString()
- str3 = demoRon.logName
+ str3 = demoNotif.logName
},
- { "... > Call=$str1 > Notifs=$str2 > DemoRon=$str3" },
+ { "... > Call=$str1 > Notifs=$str2 > DemoNotif=$str3" },
)
ChipBundle(
screenRecord = screenRecord,
@@ -139,7 +138,7 @@
castToOtherDevice = castToOtherDevice,
call = call,
notifs = notifs,
- demoRon = demoRon,
+ demoNotif = demoNotif,
)
}
// Some of the chips could have timers in them and we don't want the start time
@@ -198,9 +197,9 @@
* actually displaying the chip.
*/
val chips: StateFlow<MultipleOngoingActivityChipsModel> =
- if (!Flags.statusBarRonChips()) {
- // Multiple chips are only allowed with RONs. If the flag isn't on, use just the
- // primary chip.
+ if (!StatusBarNotifChips.isEnabled) {
+ // Multiple chips are only allowed with notification chips. If the flag isn't on, use
+ // just the primary chip.
primaryChip
.map {
MultipleOngoingActivityChipsModel(
@@ -282,11 +281,12 @@
remainingChips =
bundle.copy(notifs = bundle.notifs.subList(1, bundle.notifs.size)),
)
- bundle.demoRon is OngoingActivityChipModel.Shown -> {
- StatusBarRonChips.assertInNewMode()
+ bundle.demoNotif is OngoingActivityChipModel.Shown -> {
+ StatusBarNotifChips.assertInNewMode()
MostImportantChipResult(
- mostImportantChip = InternalChipModel.Shown(ChipType.DemoRon, bundle.demoRon),
- remainingChips = bundle.copy(demoRon = OngoingActivityChipModel.Hidden()),
+ mostImportantChip =
+ InternalChipModel.Shown(ChipType.DemoNotification, bundle.demoNotif),
+ remainingChips = bundle.copy(demoNotif = OngoingActivityChipModel.Hidden()),
)
}
else -> {
@@ -296,7 +296,7 @@
check(bundle.castToOtherDevice is OngoingActivityChipModel.Hidden)
check(bundle.call is OngoingActivityChipModel.Hidden)
check(bundle.notifs.isEmpty())
- check(bundle.demoRon is OngoingActivityChipModel.Hidden)
+ check(bundle.demoNotif is OngoingActivityChipModel.Hidden)
MostImportantChipResult(
mostImportantChip =
InternalChipModel.Hidden(
@@ -305,7 +305,7 @@
castToOtherDevice = bundle.castToOtherDevice,
call = bundle.call,
notifs = OngoingActivityChipModel.Hidden(),
- demoRon = bundle.demoRon,
+ demoNotif = bundle.demoNotif,
),
// All the chips are already hidden, so no need to filter anything out of the
// bundle.
@@ -334,7 +334,7 @@
ChipType.CastToOtherDevice -> new.castToOtherDevice
ChipType.Call -> new.call
ChipType.Notification -> new.notifs
- ChipType.DemoRon -> new.demoRon
+ ChipType.DemoNotification -> new.demoNotif
}
} else if (new is InternalChipModel.Shown) {
// If we have a chip to show, always show it.
@@ -356,7 +356,7 @@
castToOtherDevice = OngoingActivityChipModel.Hidden(),
call = OngoingActivityChipModel.Hidden(),
notifs = OngoingActivityChipModel.Hidden(),
- demoRon = OngoingActivityChipModel.Hidden(),
+ demoNotif = OngoingActivityChipModel.Hidden(),
)
private val DEFAULT_MULTIPLE_INTERNAL_HIDDEN_MODEL =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
index 525f3de..72cd63f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.dagger;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.emergency.EmergencyGestureModule;
import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
import com.android.systemui.statusbar.notification.row.NotificationRowModule;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -32,7 +31,7 @@
*/
@Module(includes = {CentralSurfacesDependenciesModule.class,
StatusBarNotificationPresenterModule.class,
- NotificationsModule.class, NotificationRowModule.class, EmergencyGestureModule.class})
+ NotificationsModule.class, NotificationRowModule.class})
public interface CentralSurfacesModule {
/**
* Provides our instance of CentralSurfaces which is considered optional.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 5900fb0..697a6ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -17,7 +17,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
@@ -78,7 +78,7 @@
/** The notifications that are promoted and ongoing. Sorted by priority order. */
val promotedOngoingNotifications: Flow<List<ActiveNotificationModel>> =
- if (StatusBarRonChips.isEnabled) {
+ if (StatusBarNotifChips.isEnabled) {
// TODO(b/364653005): Filter all the notifications down to just the promoted ones.
// TODO(b/364653005): [ongoingCallNotification] should be incorporated into this flow
// instead of being separate.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index e5ce25d..bf30322 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -309,7 +309,20 @@
}
}
- /** Set onClickListener for the manage/history button. */
+ /** Set onClickListener for the notification settings button. */
+ public void setSettingsButtonClickListener(OnClickListener listener) {
+ mSettingsButton.setOnClickListener(listener);
+ }
+
+ /** Set onClickListener for the notification history button. */
+ public void setHistoryButtonClickListener(OnClickListener listener) {
+ mHistoryButton.setOnClickListener(listener);
+ }
+
+ /**
+ * Set onClickListener for the manage/history button. This is replaced by two separate buttons
+ * in the redesign.
+ */
public void setManageButtonClickListener(OnClickListener listener) {
mManageOrHistoryButton.setOnClickListener(listener);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index ddd9cdd..34894a2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -18,9 +18,12 @@
import android.view.View
import androidx.lifecycle.lifecycleScope
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.Flags
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.notification.NotificationActivityStarter
+import com.android.systemui.statusbar.notification.NotificationActivityStarter.SettingsIntent
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
@@ -29,7 +32,6 @@
import com.android.systemui.util.ui.value
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.coroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Binds a [FooterView] to its [view model][FooterViewModel]. */
object FooterViewBinder {
@@ -74,6 +76,9 @@
notificationActivityStarter,
)
}
+ } else {
+ bindSettingsButtonListener(footer, notificationActivityStarter)
+ bindHistoryButtonListener(footer, notificationActivityStarter)
}
launch { bindMessage(footer, viewModel) }
}
@@ -117,6 +122,34 @@
}
}
+ private fun bindSettingsButtonListener(
+ footer: FooterView,
+ notificationActivityStarter: NotificationActivityStarter,
+ ) {
+ val settingsIntent =
+ SettingsIntent.forNotificationSettings(
+ cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+ )
+ val onClickListener = { view: View ->
+ notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+ }
+ footer.setSettingsButtonClickListener(onClickListener)
+ }
+
+ private fun bindHistoryButtonListener(
+ footer: FooterView,
+ notificationActivityStarter: NotificationActivityStarter,
+ ) {
+ val settingsIntent =
+ SettingsIntent.forNotificationHistory(
+ cujType = InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON
+ )
+ val onClickListener = { view: View ->
+ notificationActivityStarter.startSettingsIntent(view, settingsIntent)
+ }
+ footer.setHistoryButtonClickListener(onClickListener)
+ }
+
private suspend fun bindManageOrHistoryButton(
footer: FooterView,
viewModel: FooterViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index a3f4cd2..d8021fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -98,7 +98,6 @@
val manageButtonShouldLaunchHistory =
notificationSettingsInteractor.isNotificationHistoryEnabled
- // TODO(b/366003631): When inlining the flag, consider adding this to FooterButtonViewModel.
val manageOrHistoryButtonClick: Flow<SettingsIntent> by lazy {
if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
flowOf(SettingsIntent(Intent(Settings.ACTION_NOTIFICATION_SETTINGS)))
@@ -124,7 +123,11 @@
else R.string.manage_notifications_text
}
- /** The button for managing notification settings or opening notification history. */
+ /**
+ * The button for managing notification settings or opening notification history. This is
+ * replaced by two separate buttons in the redesign. These are currently static, and therefore
+ * not modeled here, but if that changes we can also add them as FooterButtonViewModels.
+ */
val manageOrHistoryButton: FooterButtonViewModel =
FooterButtonViewModel(
labelId = manageOrHistoryButtonText,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index dde83b9..c1d72e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -6953,12 +6953,10 @@
/** Use {@link ScrollViewFields#intrinsicStackHeight}, when SceneContainerFlag is enabled. */
private int getContentHeight() {
- SceneContainerFlag.assertInLegacyMode();
return mContentHeight;
}
private void setContentHeight(int contentHeight) {
- SceneContainerFlag.assertInLegacyMode();
mContentHeight = contentHeight;
}
@@ -6967,12 +6965,10 @@
* @return the height of the content ignoring the footer.
*/
public float getIntrinsicContentHeight() {
- SceneContainerFlag.assertInLegacyMode();
return mIntrinsicContentHeight;
}
private void setIntrinsicContentHeight(float intrinsicContentHeight) {
- SceneContainerFlag.assertInLegacyMode();
mIntrinsicContentHeight = intrinsicContentHeight;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index f85785f..013141b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -56,7 +56,7 @@
import com.android.systemui.statusbar.OperatorNameView;
import com.android.systemui.statusbar.OperatorNameViewController;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips;
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
import com.android.systemui.statusbar.core.StatusBarSimpleFragment;
import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
@@ -657,7 +657,7 @@
}
boolean showSecondaryOngoingActivityChip =
Flags.statusBarScreenSharingChips()
- && StatusBarRonChips.isEnabled()
+ && StatusBarNotifChips.isEnabled()
&& mHasSecondaryOngoingActivity;
return new StatusBarVisibilityModel(
@@ -699,7 +699,7 @@
boolean showSecondaryOngoingActivityChip =
// Secondary chips are only supported when RONs are enabled.
- StatusBarRonChips.isEnabled()
+ StatusBarNotifChips.isEnabled()
&& visibilityModel.getShowSecondaryOngoingActivityChip()
&& !disableNotifications;
if (showSecondaryOngoingActivityChip) {
@@ -832,7 +832,7 @@
}
private void showSecondaryOngoingActivityChip(boolean animate) {
- StatusBarRonChips.assertInNewMode();
+ StatusBarNotifChips.assertInNewMode();
StatusBarSimpleFragment.assertInLegacyMode();
animateShow(mSecondaryOngoingActivityChip, animate);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
index 473f956..11d7339 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt
@@ -27,6 +27,7 @@
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.binder.OngoingActivityChipBinder
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.core.StatusBarSimpleFragment
@@ -84,7 +85,7 @@
}
}
- if (Flags.statusBarScreenSharingChips() && !Flags.statusBarRonChips()) {
+ if (Flags.statusBarScreenSharingChips() && !StatusBarNotifChips.isEnabled) {
val primaryChipView: View =
view.requireViewById(R.id.ongoing_activity_chip_primary)
launch {
@@ -120,7 +121,7 @@
}
}
- if (Flags.statusBarScreenSharingChips() && Flags.statusBarRonChips()) {
+ if (Flags.statusBarScreenSharingChips() && StatusBarNotifChips.isEnabled) {
val primaryChipView: View =
view.requireViewById(R.id.ongoing_activity_chip_primary)
val secondaryChipView: View =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index 8aaa121..3c922dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -13,6 +13,7 @@
*/
package com.android.systemui.qs
+import android.content.res.Configuration
import android.graphics.Rect
import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableContext
@@ -91,7 +92,9 @@
@After
fun tearDown() {
- ViewUtils.detachView(qsPanel)
+ if (qsPanel.isAttachedToWindow) {
+ ViewUtils.detachView(qsPanel)
+ }
}
@Test
@@ -119,7 +122,7 @@
qsPanel.tileLayout?.addTile(
QSPanelControllerBase.TileRecord(
mock(QSTile::class.java),
- QSTileViewImpl(themedContext)
+ QSTileViewImpl(themedContext),
)
)
@@ -129,7 +132,7 @@
qsPanel.setUsingHorizontalLayout(/* horizontal */ true, mediaView, /* force */ true)
qsPanel.measure(
/* width */ View.MeasureSpec.makeMeasureSpec(3000, View.MeasureSpec.EXACTLY),
- /* height */ View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)
+ /* height */ View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY),
)
qsPanel.layout(0, 0, qsPanel.measuredWidth, qsPanel.measuredHeight)
@@ -147,7 +150,7 @@
val padding = 10
themedContext.orCreateTestableResources.addOverride(
R.dimen.qs_panel_padding_bottom,
- padding
+ padding,
)
qsPanel.updatePadding()
assertThat(qsPanel.paddingBottom).isEqualTo(padding)
@@ -160,7 +163,7 @@
themedContext.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_top, padding)
themedContext.orCreateTestableResources.addOverride(
R.dimen.qs_panel_padding_top,
- paddingCombined
+ paddingCombined,
)
qsPanel.updatePadding()
@@ -267,6 +270,62 @@
assertThat(qsPanel.tileLayout!!.maxColumns).isEqualTo(2)
}
+ @Test
+ fun noPendingConfigChangesAtBeginning() {
+ assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse()
+ }
+
+ @Test
+ fun configChangesWhileDetached_pendingConfigChanges() {
+ ViewUtils.detachView(qsPanel)
+
+ qsPanel.onConfigurationChanged(Configuration())
+
+ assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue()
+ }
+
+ @Test
+ fun configChangesWhileDetached_reattach_pendingConfigChanges() {
+ ViewUtils.detachView(qsPanel)
+
+ qsPanel.onConfigurationChanged(Configuration())
+ testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) }
+
+ assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue()
+ }
+
+ @Test
+ fun configChangesWhileDetached_reattach_detach_pendingConfigChanges_reset() {
+ ViewUtils.detachView(qsPanel)
+
+ qsPanel.onConfigurationChanged(Configuration())
+
+ testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) }
+ ViewUtils.detachView(qsPanel)
+
+ assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse()
+ }
+
+ @Test
+ fun configChangeWhileAttached_noPendingConfigChanges() {
+ qsPanel.onConfigurationChanged(Configuration())
+
+ assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isFalse()
+ }
+
+ @Test
+ fun configChangeWhileAttachedWithPending_doesntResetPending() {
+ ViewUtils.detachView(qsPanel)
+
+ qsPanel.onConfigurationChanged(Configuration())
+
+ testableLooper.runWithLooper { ViewUtils.attachView(qsPanel) }
+
+ qsPanel.onConfigurationChanged(Configuration())
+
+ assertThat(qsPanel.hadConfigurationChangeWhileDetached()).isTrue()
+ }
+
companion object {
@Parameters(name = "{0}") @JvmStatic fun getParams() = parameterizeSceneContainerFlag()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index e26a7ea..d01c1ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -16,7 +16,6 @@
import static android.view.Display.DEFAULT_DISPLAY;
-import static com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS;
import static com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS;
import static com.android.systemui.Flags.FLAG_STATUS_BAR_SIMPLE_FRAGMENT;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -61,6 +60,7 @@
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.OperatorNameViewController;
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips;
import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder;
@@ -633,7 +633,7 @@
@Test
@EnableFlags({
FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS,
- FLAG_STATUS_BAR_RON_CHIPS,
+ StatusBarNotifChips.FLAG_NAME,
FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
public void hasPrimaryOngoingActivity_viewsUnchangedWhenSimpleFragmentFlagOn() {
resumeAndGetFragment();
@@ -660,8 +660,8 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
- public void hasSecondaryOngoingActivity_butRonsFlagOff_secondaryChipHidden() {
+ @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+ public void hasSecondaryOngoingActivity_butNotifsFlagOff_secondaryChipHidden() {
resumeAndGetFragment();
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -673,7 +673,7 @@
}
@Test
- @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+ @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
public void hasSecondaryOngoingActivity_flagOn_secondaryChipShownAndNotificationIconsHidden() {
resumeAndGetFragment();
@@ -689,8 +689,8 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
- public void hasOngoingActivityButNotificationIconsDisabled_chipHidden_ronsFlagOff() {
+ @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+ public void hasOngoingActivityButNotificationIconsDisabled_chipHidden_notifsFlagOff() {
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -705,9 +705,9 @@
}
@Test
- @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+ @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
- public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_ronsFlagOn() {
+ public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_notifsFlagOn() {
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -724,8 +724,8 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
- public void hasOngoingActivityButAlsoHun_chipHidden_ronsFlagOff() {
+ @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+ public void hasOngoingActivityButAlsoHun_chipHidden_notifsFlagOff() {
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -740,9 +740,9 @@
}
@Test
- @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+ @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
- public void hasOngoingActivitiesButAlsoHun_chipsHidden_ronsFlagOn() {
+ public void hasOngoingActivitiesButAlsoHun_chipsHidden_notifsFlagOn() {
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged(
@@ -759,8 +759,8 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
- public void primaryOngoingActivityEnded_chipHidden_ronsFlagOff() {
+ @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+ public void primaryOngoingActivityEnded_chipHidden_notifsFlagOff() {
resumeAndGetFragment();
// Ongoing activity started
@@ -781,9 +781,9 @@
}
@Test
- @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+ @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
- public void primaryOngoingActivityEnded_chipHidden_ronsFlagOn() {
+ public void primaryOngoingActivityEnded_chipHidden_notifsFlagOn() {
resumeAndGetFragment();
// Ongoing activity started
@@ -804,7 +804,7 @@
}
@Test
- @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+ @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
public void secondaryOngoingActivityEnded_chipHidden() {
resumeAndGetFragment();
@@ -828,8 +828,8 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
- public void hasOngoingActivity_hidesNotifsWithoutAnimation_ronsFlagOff() {
+ @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+ public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOff() {
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
// Enable animations for testing so that we can verify we still aren't animating
fragment.enableAnimationsForTesting();
@@ -846,9 +846,9 @@
}
@Test
- @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+ @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
- public void hasOngoingActivity_hidesNotifsWithoutAnimation_ronsFlagOn() {
+ public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOn() {
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
// Enable animations for testing so that we can verify we still aren't animating
fragment.enableAnimationsForTesting();
@@ -866,8 +866,8 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
- @DisableFlags({FLAG_STATUS_BAR_RON_CHIPS, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
- public void screenSharingChipsEnabled_ignoresOngoingCallController_ronsFlagOff() {
+ @DisableFlags({StatusBarNotifChips.FLAG_NAME, FLAG_STATUS_BAR_SIMPLE_FRAGMENT})
+ public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOff() {
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
// WHEN there *is* an ongoing call via old callback
@@ -898,9 +898,9 @@
}
@Test
- @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, FLAG_STATUS_BAR_RON_CHIPS})
+ @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME})
@DisableFlags(FLAG_STATUS_BAR_SIMPLE_FRAGMENT)
- public void screenSharingChipsEnabled_ignoresOngoingCallController_ronsFlagOn() {
+ public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOn() {
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
// WHEN there *is* an ongoing call via old callback
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelKosmos.kt
similarity index 85%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelKosmos.kt
index c0d65a0..2316a2f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/demo/ui/viewmodel/DemoNotifChipViewModelKosmos.kt
@@ -14,16 +14,16 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel
+package com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel
import android.content.packageManager
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.statusbar.commandline.commandRegistry
import com.android.systemui.util.time.fakeSystemClock
-val Kosmos.demoRonChipViewModel: DemoRonChipViewModel by
+val Kosmos.demoNotifChipViewModel: DemoNotifChipViewModel by
Kosmos.Fixture {
- DemoRonChipViewModel(
+ DemoNotifChipViewModel(
commandRegistry = commandRegistry,
packageManager = packageManager,
systemClock = fakeSystemClock,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
similarity index 85%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/ui/viewmodel/NotifChipsViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
index 11d1cb7..af24c37 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/ui/viewmodel/NotifChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.chips.ron.ui.viewmodel
+package com.android.systemui.statusbar.chips.notification.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModel
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
val Kosmos.notifChipsViewModel: NotifChipsViewModel by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
index b2be0b2..0300bf4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
@@ -20,8 +20,8 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.chips.call.ui.viewmodel.callChipViewModel
import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.castToOtherDeviceChipViewModel
-import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel
-import com.android.systemui.statusbar.chips.ron.ui.viewmodel.notifChipsViewModel
+import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.demoNotifChipViewModel
+import com.android.systemui.statusbar.chips.notification.ui.viewmodel.notifChipsViewModel
import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.screenRecordChipViewModel
import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel
import com.android.systemui.statusbar.chips.statusBarChipsLogger
@@ -35,7 +35,7 @@
castToOtherDeviceChipViewModel = castToOtherDeviceChipViewModel,
callChipViewModel = callChipViewModel,
notifChipsViewModel = notifChipsViewModel,
- demoRonChipViewModel = demoRonChipViewModel,
+ demoNotifChipViewModel = demoNotifChipViewModel,
logger = statusBarChipsLogger,
)
}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 51034d2..384cf46 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -31,16 +31,13 @@
import static com.android.internal.util.CollectionUtils.any;
import static com.android.internal.util.Preconditions.checkState;
-import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
-import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.MINUTES;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
@@ -69,31 +66,22 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.net.MacAddress;
-import android.net.NetworkPolicyManager;
import android.os.Binder;
-import android.os.Environment;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.PowerExemptionManager;
import android.os.PowerManagerInternal;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.permission.flags.Flags;
-import android.util.ArraySet;
import android.util.ExceptionUtils;
import android.util.Slog;
-import com.android.internal.app.IAppOpsService;
import com.android.internal.content.PackageMonitor;
import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
-import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
@@ -114,35 +102,27 @@
import com.android.server.companion.devicepresence.ObservableUuid;
import com.android.server.companion.devicepresence.ObservableUuidStore;
import com.android.server.companion.transport.CompanionTransportManager;
-import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal;
-import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
-import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
@SuppressLint("LongLogTag")
public class CompanionDeviceManagerService extends SystemService {
private static final String TAG = "CDM_CompanionDeviceManagerService";
private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min
-
- private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
- private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
private static final int MAX_CN_LENGTH = 500;
- private final ActivityTaskManagerInternal mAtmInternal;
- private final ActivityManagerInternal mAmInternal;
- private final IAppOpsService mAppOpsManager;
- private final PowerExemptionManager mPowerExemptionManager;
- private final PackageManagerInternal mPackageManagerInternal;
-
private final AssociationStore mAssociationStore;
private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
private final ObservableUuidStore mObservableUuidStore;
+
+ private final CompanionExemptionProcessor mCompanionExemptionProcessor;
private final AssociationRequestsProcessor mAssociationRequestsProcessor;
private final SystemDataTransferProcessor mSystemDataTransferProcessor;
private final BackupRestoreProcessor mBackupRestoreProcessor;
@@ -156,12 +136,15 @@
super(context);
final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
- mPowerExemptionManager = context.getSystemService(PowerExemptionManager.class);
- mAppOpsManager = IAppOpsService.Stub.asInterface(
- ServiceManager.getService(Context.APP_OPS_SERVICE));
- mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
- mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
- mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+ final PowerExemptionManager powerExemptionManager = context.getSystemService(
+ PowerExemptionManager.class);
+ final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+ final ActivityTaskManagerInternal atmInternal = LocalServices.getService(
+ ActivityTaskManagerInternal.class);
+ final ActivityManagerInternal amInternal = LocalServices.getService(
+ ActivityManagerInternal.class);
+ final PackageManagerInternal packageManagerInternal = LocalServices.getService(
+ PackageManagerInternal.class);
final UserManager userManager = context.getSystemService(UserManager.class);
final PowerManagerInternal powerManagerInternal = LocalServices.getService(
PowerManagerInternal.class);
@@ -173,25 +156,29 @@
// Init processors
mAssociationRequestsProcessor = new AssociationRequestsProcessor(context,
- mPackageManagerInternal, mAssociationStore);
- mBackupRestoreProcessor = new BackupRestoreProcessor(context, mPackageManagerInternal,
+ packageManagerInternal, mAssociationStore);
+ mBackupRestoreProcessor = new BackupRestoreProcessor(context, packageManagerInternal,
mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore,
mAssociationRequestsProcessor);
mCompanionAppBinder = new CompanionAppBinder(context);
+ mCompanionExemptionProcessor = new CompanionExemptionProcessor(context,
+ powerExemptionManager, appOpsManager, packageManagerInternal, atmInternal,
+ amInternal, mAssociationStore);
+
mDevicePresenceProcessor = new DevicePresenceProcessor(context,
mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore,
- powerManagerInternal);
+ powerManagerInternal, mCompanionExemptionProcessor);
mTransportManager = new CompanionTransportManager(context, mAssociationStore);
mDisassociationProcessor = new DisassociationProcessor(context, activityManager,
- mAssociationStore, mPackageManagerInternal, mDevicePresenceProcessor,
+ mAssociationStore, packageManagerInternal, mDevicePresenceProcessor,
mCompanionAppBinder, mSystemDataTransferRequestStore, mTransportManager);
mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
- mPackageManagerInternal, mAssociationStore,
+ packageManagerInternal, mAssociationStore,
mSystemDataTransferRequestStore, mTransportManager);
// TODO(b/279663946): move context sync to a dedicated system service
@@ -202,7 +189,6 @@
public void onStart() {
// Init association stores
mAssociationStore.refreshCache();
- mAssociationStore.registerLocalListener(mAssociationStoreChangeListener);
// Init UUID store
mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId());
@@ -240,11 +226,11 @@
if (associations.isEmpty()) return;
- updateAtm(userId, associations);
+ mCompanionExemptionProcessor.updateAtm(userId, associations);
- BackgroundThread.getHandler().sendMessageDelayed(
- obtainMessage(CompanionDeviceManagerService::maybeGrantAutoRevokeExemptions, this),
- MINUTES.toMillis(10));
+ try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
+ executor.execute(mCompanionExemptionProcessor::updateAutoRevokeExemptions);
+ }
}
@Override
@@ -274,18 +260,16 @@
mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName);
}
- mCompanionAppBinder.onPackagesChanged(userId);
+ mCompanionAppBinder.onPackagesChanged(userId, packageName);
}
private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) {
- final List<AssociationInfo> associationsForPackage =
+ final List<AssociationInfo> associations =
mAssociationStore.getAssociationsByPackage(userId, packageName);
- for (AssociationInfo association : associationsForPackage) {
- updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
- association.getPackageName());
+ if (!associations.isEmpty()) {
+ mCompanionExemptionProcessor.exemptPackage(userId, packageName, false);
+ mCompanionAppBinder.onPackagesChanged(userId, packageName);
}
-
- mCompanionAppBinder.onPackagesChanged(userId);
}
private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) {
@@ -765,130 +749,6 @@
}
}
- /**
- * Update special access for the association's package
- */
- public void updateSpecialAccessPermissionForAssociatedPackage(int userId, String packageName) {
- final PackageInfo packageInfo =
- getPackageInfo(getContext(), userId, packageName);
-
- Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo));
- }
-
- private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
- if (packageInfo == null) {
- return;
- }
-
- if (containsEither(packageInfo.requestedPermissions,
- android.Manifest.permission.RUN_IN_BACKGROUND,
- android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
- mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName);
- } else {
- try {
- mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName);
- } catch (UnsupportedOperationException e) {
- Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
- + " whitelist. It might due to the package is whitelisted by the system.");
- }
- }
-
- NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext());
- try {
- if (containsEither(packageInfo.requestedPermissions,
- android.Manifest.permission.USE_DATA_IN_BACKGROUND,
- android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) {
- networkPolicyManager.addUidPolicy(
- packageInfo.applicationInfo.uid,
- NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
- } else {
- networkPolicyManager.removeUidPolicy(
- packageInfo.applicationInfo.uid,
- NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
- }
- } catch (IllegalArgumentException e) {
- Slog.e(TAG, e.getMessage());
- }
-
- exemptFromAutoRevoke(packageInfo.packageName, packageInfo.applicationInfo.uid);
- }
-
- private void exemptFromAutoRevoke(String packageName, int uid) {
- try {
- mAppOpsManager.setMode(
- AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
- uid,
- packageName,
- AppOpsManager.MODE_IGNORED);
- } catch (RemoteException e) {
- Slog.w(TAG, "Error while granting auto revoke exemption for " + packageName, e);
- }
- }
-
- private void updateAtm(int userId, List<AssociationInfo> associations) {
- final Set<Integer> companionAppUids = new ArraySet<>();
- for (AssociationInfo association : associations) {
- final int uid = mPackageManagerInternal.getPackageUid(association.getPackageName(),
- 0, userId);
- if (uid >= 0) {
- companionAppUids.add(uid);
- }
- }
- if (mAtmInternal != null) {
- mAtmInternal.setCompanionAppUids(userId, companionAppUids);
- }
- if (mAmInternal != null) {
- // Make a copy of the set and send it to ActivityManager.
- mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
- }
- }
-
- private void maybeGrantAutoRevokeExemptions() {
- Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");
-
- PackageManager pm = getContext().getPackageManager();
- for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
- SharedPreferences pref = getContext().getSharedPreferences(
- new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
- Context.MODE_PRIVATE);
- if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
- continue;
- }
-
- try {
- final List<AssociationInfo> associations =
- mAssociationStore.getActiveAssociationsByUser(userId);
- for (AssociationInfo a : associations) {
- try {
- int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
- exemptFromAutoRevoke(a.getPackageName(), uid);
- } catch (PackageManager.NameNotFoundException e) {
- Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
- }
- }
- } finally {
- pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
- }
- }
- }
-
- private final AssociationStore.OnChangeListener mAssociationStoreChangeListener =
- new AssociationStore.OnChangeListener() {
- @Override
- public void onAssociationChanged(int changeType, AssociationInfo association) {
- Slog.d(TAG, "onAssociationChanged changeType=[" + changeType
- + "], association=[" + association);
-
- final int userId = association.getUserId();
- final List<AssociationInfo> updatedAssociations =
- mAssociationStore.getActiveAssociationsByUser(userId);
-
- updateAtm(userId, updatedAssociations);
- updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(),
- association.getPackageName());
- }
- };
-
private final PackageMonitor mPackageMonitor = new PackageMonitor() {
@Override
public void onPackageRemoved(String packageName, int uid) {
@@ -911,10 +771,6 @@
}
};
- private static <T> boolean containsEither(T[] array, T a, T b) {
- return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
- }
-
private class LocalService implements CompanionDeviceManagerServiceInternal {
@Override
diff --git a/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java b/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java
new file mode 100644
index 0000000..6ddb569
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_IGNORED;
+
+import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManagerInternal;
+import android.app.AppOpsManager;
+import android.companion.AssociationInfo;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.net.NetworkPolicyManager;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.PowerExemptionManager;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.server.LocalServices;
+import com.android.server.companion.association.AssociationStore;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+
+@SuppressLint("LongLogTag")
+public class CompanionExemptionProcessor {
+
+ private static final String TAG = "CDM_CompanionExemptionProcessor";
+
+ private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
+ private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
+
+ private final Context mContext;
+ private final PowerExemptionManager mPowerExemptionManager;
+ private final AppOpsManager mAppOpsManager;
+ private final PackageManagerInternal mPackageManager;
+ private final ActivityTaskManagerInternal mAtmInternal;
+ private final ActivityManagerInternal mAmInternal;
+ private final AssociationStore mAssociationStore;
+
+ public CompanionExemptionProcessor(Context context, PowerExemptionManager powerExemptionManager,
+ AppOpsManager appOpsManager, PackageManagerInternal packageManager,
+ ActivityTaskManagerInternal atmInternal, ActivityManagerInternal amInternal,
+ AssociationStore associationStore) {
+ mContext = context;
+ mPowerExemptionManager = powerExemptionManager;
+ mAppOpsManager = appOpsManager;
+ mPackageManager = packageManager;
+ mAtmInternal = atmInternal;
+ mAmInternal = amInternal;
+ mAssociationStore = associationStore;
+
+ mAssociationStore.registerLocalListener(new AssociationStore.OnChangeListener() {
+ @Override
+ public void onAssociationChanged(int changeType, AssociationInfo association) {
+ final int userId = association.getUserId();
+ final List<AssociationInfo> updatedAssociations =
+ mAssociationStore.getActiveAssociationsByUser(userId);
+
+ updateAtm(userId, updatedAssociations);
+ }
+ });
+ }
+
+ /**
+ * Update ActivityManager and ActivityTaskManager exemptions
+ */
+ public void updateAtm(int userId, List<AssociationInfo> associations) {
+ final Set<Integer> companionAppUids = new ArraySet<>();
+ for (AssociationInfo association : associations) {
+ int uid = mPackageManager.getPackageUid(association.getPackageName(), 0, userId);
+ if (uid >= 0) {
+ companionAppUids.add(uid);
+ }
+ }
+ if (mAtmInternal != null) {
+ mAtmInternal.setCompanionAppUids(userId, companionAppUids);
+ }
+ if (mAmInternal != null) {
+ // Make a copy of the set and send it to ActivityManager.
+ mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
+ }
+ }
+
+ /**
+ * Update special access for the association's package
+ */
+ public void exemptPackage(int userId, String packageName, boolean hasPresentDevices) {
+ final PackageInfo packageInfo = getPackageInfo(mContext, userId, packageName);
+
+ Binder.withCleanCallingIdentity(
+ () -> exemptPackageAsSystem(userId, packageInfo, hasPresentDevices));
+ }
+
+ @SuppressLint("MissingPermission")
+ private void exemptPackageAsSystem(int userId, PackageInfo packageInfo,
+ boolean hasPresentDevices) {
+ if (packageInfo == null) {
+ return;
+ }
+
+ // If the app has run-in-bg permission and present devices, add it to power saver allowlist.
+ if (containsEither(packageInfo.requestedPermissions,
+ android.Manifest.permission.RUN_IN_BACKGROUND,
+ android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)
+ && hasPresentDevices) {
+ mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName);
+ } else {
+ try {
+ mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName);
+ } catch (UnsupportedOperationException e) {
+ Slog.w(TAG, packageInfo.packageName + " can't be removed from power save"
+ + " allowlist. It might be due to the package being allowlisted by the"
+ + " system.");
+ }
+ }
+
+ // If the app has run-in-bg permission and present device, allow metered network use.
+ NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(mContext);
+ try {
+ if (containsEither(packageInfo.requestedPermissions,
+ android.Manifest.permission.USE_DATA_IN_BACKGROUND,
+ android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)
+ && hasPresentDevices) {
+ networkPolicyManager.addUidPolicy(
+ packageInfo.applicationInfo.uid,
+ NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+ } else {
+ networkPolicyManager.removeUidPolicy(
+ packageInfo.applicationInfo.uid,
+ NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND);
+ }
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, e.getMessage());
+ }
+
+ updateAutoRevokeExemption(packageInfo.packageName, packageInfo.applicationInfo.uid,
+ !mAssociationStore.getActiveAssociationsByPackage(userId,
+ packageInfo.packageName).isEmpty());
+ }
+
+ /**
+ * Update auto revoke exemptions.
+ * If the app has any association, exempt it from permission auto revoke.
+ */
+ public void updateAutoRevokeExemptions() {
+ Slog.d(TAG, "maybeGrantAutoRevokeExemptions()");
+
+ PackageManager pm = mContext.getPackageManager();
+ for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) {
+ SharedPreferences pref = mContext.getSharedPreferences(
+ new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME),
+ Context.MODE_PRIVATE);
+ if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) {
+ continue;
+ }
+
+ try {
+ final List<AssociationInfo> associations =
+ mAssociationStore.getActiveAssociationsByUser(userId);
+ for (AssociationInfo a : associations) {
+ try {
+ int uid = pm.getPackageUidAsUser(a.getPackageName(), userId);
+ updateAutoRevokeExemption(a.getPackageName(), uid, true);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e);
+ }
+ }
+ } finally {
+ pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply();
+ }
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private void updateAutoRevokeExemption(String packageName, int uid, boolean hasAssociations) {
+ try {
+ mAppOpsManager.setMode(
+ AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
+ uid,
+ packageName,
+ hasAssociations ? MODE_IGNORED : MODE_ALLOWED);
+ } catch (Exception e) {
+ Slog.e(TAG, "Error while granting auto revoke exemption for " + packageName, e);
+ }
+ }
+
+ private <T> boolean containsEither(T[] array, T a, T b) {
+ return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
+ }
+
+}
diff --git a/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
index 60f4688..48accbf 100644
--- a/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
+++ b/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java
@@ -95,8 +95,8 @@
/**
* On package changed.
*/
- public void onPackagesChanged(@UserIdInt int userId) {
- mCompanionServicesRegister.invalidate(userId);
+ public void onPackagesChanged(@UserIdInt int userId, String packageName) {
+ mCompanionServicesRegister.forUser(userId).remove(packageName);
}
/**
@@ -309,10 +309,6 @@
return forUser(userId).getOrDefault(packageName, Collections.emptyList());
}
- synchronized void invalidate(@UserIdInt int userId) {
- remove(userId);
- }
-
@Override
protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) {
return PackageUtils.getCompanionServicesForUser(mContext, userId);
diff --git a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
index a374d27..7b4dd7d 100644
--- a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
+++ b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java
@@ -57,6 +57,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.CollectionUtils;
+import com.android.server.companion.CompanionExemptionProcessor;
import com.android.server.companion.association.AssociationStore;
import java.io.PrintWriter;
@@ -101,6 +102,8 @@
private final PowerManagerInternal mPowerManagerInternal;
@NonNull
private final UserManager mUserManager;
+ @NonNull
+ private final CompanionExemptionProcessor mCompanionExemptionProcessor;
// NOTE: Same association may appear in more than one of the following sets at the same time.
// (E.g. self-managed devices that have MAC addresses, could be reported as present by their
@@ -111,7 +114,7 @@
@NonNull
private final Set<Integer> mNearbyBleDevices = new HashSet<>();
@NonNull
- private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
+ private final Set<Integer> mConnectedSelfManagedDevices = new HashSet<>();
@NonNull
private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
@NonNull
@@ -146,7 +149,8 @@
@NonNull UserManager userManager,
@NonNull AssociationStore associationStore,
@NonNull ObservableUuidStore observableUuidStore,
- @NonNull PowerManagerInternal powerManagerInternal) {
+ @NonNull PowerManagerInternal powerManagerInternal,
+ @NonNull CompanionExemptionProcessor companionExemptionProcessor) {
mContext = context;
mCompanionAppBinder = companionAppBinder;
mAssociationStore = associationStore;
@@ -156,6 +160,7 @@
mObservableUuidStore, this);
mBleDeviceProcessor = new BleDeviceProcessor(associationStore, this);
mPowerManagerInternal = powerManagerInternal;
+ mCompanionExemptionProcessor = companionExemptionProcessor;
}
/** Initialize {@link DevicePresenceProcessor} */
@@ -404,7 +409,7 @@
* nearby (for "self-managed" associations).
*/
public boolean isDevicePresent(int associationId) {
- return mReportedSelfManagedDevices.contains(associationId)
+ return mConnectedSelfManagedDevices.contains(associationId)
|| mConnectedBtDevices.contains(associationId)
|| mNearbyBleDevices.contains(associationId)
|| mSimulated.contains(associationId);
@@ -451,7 +456,7 @@
* notifyDeviceAppeared()}
*/
public void onSelfManagedDeviceConnected(int associationId) {
- onDevicePresenceEvent(mReportedSelfManagedDevices,
+ onDevicePresenceEvent(mConnectedSelfManagedDevices,
associationId, EVENT_SELF_MANAGED_APPEARED);
}
@@ -467,7 +472,7 @@
* notifyDeviceDisappeared()}
*/
public void onSelfManagedDeviceDisconnected(int associationId) {
- onDevicePresenceEvent(mReportedSelfManagedDevices,
+ onDevicePresenceEvent(mConnectedSelfManagedDevices,
associationId, EVENT_SELF_MANAGED_DISAPPEARED);
}
@@ -475,7 +480,7 @@
* Marks a "self-managed" device as disconnected when binderDied.
*/
public void onSelfManagedDeviceReporterBinderDied(int associationId) {
- onDevicePresenceEvent(mReportedSelfManagedDevices,
+ onDevicePresenceEvent(mConnectedSelfManagedDevices,
associationId, EVENT_SELF_MANAGED_DISAPPEARED);
}
@@ -683,6 +688,7 @@
if (association.shouldBindWhenPresent()) {
bindApplicationIfNeeded(userId, packageName, association.isSelfManaged());
+ mCompanionExemptionProcessor.exemptPackage(userId, packageName, true);
} else {
return;
}
@@ -715,6 +721,7 @@
// Check if there are other devices associated to the app that are present.
if (!shouldBindPackage(userId, packageName)) {
mCompanionAppBinder.unbindCompanionApp(userId, packageName);
+ mCompanionExemptionProcessor.exemptPackage(userId, packageName, false);
}
break;
default:
@@ -940,7 +947,7 @@
mConnectedBtDevices.remove(id);
mNearbyBleDevices.remove(id);
- mReportedSelfManagedDevices.remove(id);
+ mConnectedSelfManagedDevices.remove(id);
mSimulated.remove(id);
synchronized (mBtDisconnectedDevices) {
mBtDisconnectedDevices.remove(id);
@@ -1100,7 +1107,7 @@
out.append("Companion Device Present: ");
if (mConnectedBtDevices.isEmpty()
&& mNearbyBleDevices.isEmpty()
- && mReportedSelfManagedDevices.isEmpty()) {
+ && mConnectedSelfManagedDevices.isEmpty()) {
out.append("<empty>\n");
return;
} else {
@@ -1130,11 +1137,11 @@
}
out.append(" Self-Reported Devices: ");
- if (mReportedSelfManagedDevices.isEmpty()) {
+ if (mConnectedSelfManagedDevices.isEmpty()) {
out.append("<empty>\n");
} else {
out.append("\n");
- for (int associationId : mReportedSelfManagedDevices) {
+ for (int associationId : mConnectedSelfManagedDevices) {
AssociationInfo a = mAssociationStore.getAssociationById(associationId);
out.append(" ").append(a.toShortString()).append('\n');
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 5682c33..bf415a3 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -797,6 +797,10 @@
@Override
public void onDeviceDiscoveryDone(List<HdmiDeviceInfo> deviceInfos) {
for (HdmiDeviceInfo info : deviceInfos) {
+ if (!isInputReady(info.getDeviceId())) {
+ mService.getHdmiCecNetwork().removeCecDevice(
+ HdmiCecLocalDeviceTv.this, info.getLogicalAddress());
+ }
mService.getHdmiCecNetwork().addCecDevice(info);
}
diff --git a/services/core/java/com/android/server/inputmethod/ImeBindingState.java b/services/core/java/com/android/server/inputmethod/ImeBindingState.java
index f78ea84..5deed39 100644
--- a/services/core/java/com/android/server/inputmethod/ImeBindingState.java
+++ b/services/core/java/com/android/server/inputmethod/ImeBindingState.java
@@ -20,6 +20,7 @@
import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_FOCUSED_WINDOW_SOFT_INPUT_MODE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.os.IBinder;
@@ -86,10 +87,10 @@
InputMethodDebug.softInputModeToString(mFocusedWindowSoftInputMode));
}
- void dump(String prefix, Printer p) {
- p.println(prefix + "mFocusedWindow()=" + mFocusedWindow);
- p.println(prefix + "softInputMode=" + InputMethodDebug.softInputModeToString(
- mFocusedWindowSoftInputMode));
+ void dump(@NonNull Printer p, @NonNull String prefix) {
+ p.println(prefix + "mFocusedWindow=" + mFocusedWindow);
+ p.println(prefix + "mFocusedWindowSoftInputMode="
+ + InputMethodDebug.softInputModeToString(mFocusedWindowSoftInputMode));
p.println(prefix + "mFocusedWindowClient=" + mFocusedWindowClient);
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index ec1993a..477660d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -61,6 +61,7 @@
import com.android.server.EventLogTags;
import com.android.server.wm.WindowManagerInternal;
+import java.io.PrintWriter;
import java.util.concurrent.CountDownLatch;
/**
@@ -733,4 +734,25 @@
void setBackDisposition(@BackDispositionMode int backDisposition) {
mBackDisposition = backDisposition;
}
+
+ @GuardedBy("ImfLock.class")
+ void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
+ pw.println(prefix + "mSelectedMethodId=" + mSelectedMethodId);
+ pw.println(prefix + "mCurrentSubtype=" + mCurrentSubtype);
+ pw.println(prefix + "mCurSeq=" + mCurSeq);
+ pw.println(prefix + "mCurId=" + mCurId);
+ pw.println(prefix + "mHasMainConnection=" + mHasMainConnection);
+ pw.println(prefix + "mVisibleBound=" + mVisibleBound);
+ pw.println(prefix + "mCurToken=" + mCurToken);
+ pw.println(prefix + "mCurTokenDisplayId=" + mCurTokenDisplayId);
+ pw.println(prefix + "mCurHostInputToken=" + getCurHostInputToken());
+ pw.println(prefix + "mCurIntent=" + mCurIntent);
+ pw.println(prefix + "mCurMethod=" + mCurMethod);
+ pw.println(prefix + "mImeWindowVis=" + mImeWindowVis);
+ pw.println(prefix + "mBackDisposition=" + mBackDisposition);
+ pw.println(prefix + "mDisplayIdToShowIme=" + mDisplayIdToShowIme);
+ pw.println(prefix + "mDeviceIdToShowIme=" + mDeviceIdToShowIme);
+ pw.println(prefix + "mSupportsStylusHw=" + mSupportsStylusHw);
+ pw.println(prefix + "mSupportsConnectionlessStylusHw=" + mSupportsConnectionlessStylusHw);
+ }
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 83044c2..131b9ba 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -6063,42 +6063,40 @@
@BinderThread
@Override
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
PriorityDump.dump(mPriorityDumper, fd, pw, args);
}
@BinderThread
- private void dumpAsStringNoCheck(FileDescriptor fd, PrintWriter pw, String[] args,
- boolean isCritical) {
+ private void dumpAsStringNoCheck(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
+ @NonNull String[] args, boolean isCritical) {
final int argUserId = parseUserIdFromDumpArgs(args);
final Printer p = new PrintWriterPrinter(pw);
- p.println("Current Input Method Manager state:");
+ p.println("Input Method Manager Service state:");
p.println(" mSystemReady=" + mSystemReady);
p.println(" mInteractive=" + mIsInteractive);
p.println(" mConcurrentMultiUserModeEnabled=" + mConcurrentMultiUserModeEnabled);
p.println(" ENABLE_HIDE_IME_CAPTION_BAR="
+ InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR);
+ final int currentImeUserId;
synchronized (ImfLock.class) {
+ currentImeUserId = mCurrentImeUserId;
+ p.println(" mCurrentImeUserId=" + currentImeUserId);
p.println(" mStylusIds=" + (mStylusIds != null
? Arrays.toString(mStylusIds.toArray()) : ""));
}
+ // TODO(b/305849394): Make mMenuController multi-user aware.
if (Flags.imeSwitcherRevamp()) {
p.println(" mMenuControllerNew:");
- mMenuControllerNew.dump(p, " ");
+ mMenuControllerNew.dump(p, " ");
} else {
p.println(" mMenuController:");
- mMenuController.dump(p, " ");
+ mMenuController.dump(p, " ");
}
- if (mConcurrentMultiUserModeEnabled && argUserId == UserHandle.USER_NULL) {
- mUserDataRepository.forAllUserData(
- u -> dumpAsStringNoCheckForUser(u, fd, pw, args, isCritical));
- } else {
- final int userId = argUserId != UserHandle.USER_NULL ? argUserId : mCurrentImeUserId;
- final var userData = getUserData(userId);
- dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
- }
+ dumpClientController(p);
+ dumpUserRepository(p);
// TODO(b/365868861): Make StartInputHistory and ImeTracker multi-user aware.
synchronized (ImfLock.class) {
@@ -6112,12 +6110,18 @@
p.println(" mImeTrackerService#History:");
mImeTrackerService.dump(pw, " ");
- dumpUserRepository(p);
- dumpClientStates(p);
+ if (mConcurrentMultiUserModeEnabled && argUserId == UserHandle.USER_NULL) {
+ mUserDataRepository.forAllUserData(
+ u -> dumpAsStringNoCheckForUser(u, fd, pw, args, isCritical));
+ } else {
+ final int userId = argUserId != UserHandle.USER_NULL ? argUserId : currentImeUserId;
+ final var userData = getUserData(userId);
+ dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
+ }
}
@UserIdInt
- private static int parseUserIdFromDumpArgs(String[] args) {
+ private static int parseUserIdFromDumpArgs(@NonNull String[] args) {
final int userIdx = Arrays.binarySearch(args, "--user");
if (userIdx == -1 || userIdx == args.length - 1) {
return UserHandle.USER_NULL;
@@ -6127,44 +6131,37 @@
// TODO(b/356239178): Update dump format output to better group per-user info.
@BinderThread
- private void dumpAsStringNoCheckForUser(UserData userData, FileDescriptor fd, PrintWriter pw,
- String[] args, boolean isCritical) {
+ private void dumpAsStringNoCheckForUser(@NonNull UserData userData, @NonNull FileDescriptor fd,
+ @NonNull PrintWriter pw, @NonNull String[] args, boolean isCritical) {
final Printer p = new PrintWriterPrinter(pw);
- IInputMethodInvoker method;
ClientState client;
+ IInputMethodInvoker method;
p.println(" UserId=" + userData.mUserId);
synchronized (ImfLock.class) {
- final InputMethodSettings settings = InputMethodSettingsRepository.get(
- userData.mUserId);
+ final var bindingController = userData.mBindingController;
+ client = userData.mCurClient;
+ method = bindingController.getCurMethod();
+ p.println(" mBindingController:");
+ bindingController.dump(pw, " ");
+ p.println(" mCurClient=" + client);
+ p.println(" mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
+ p.println(" mImeBindingState:");
+ userData.mImeBindingState.dump(p, " ");
+ p.println(" mBoundToMethod=" + userData.mBoundToMethod);
+ p.println(" mEnabledSession=" + userData.mEnabledSession);
+ p.println(" mVisibilityStateComputer:");
+ userData.mVisibilityStateComputer.dump(pw, " ");
+ p.println(" mInFullscreenMode=" + userData.mInFullscreenMode);
+
+ final var settings = InputMethodSettingsRepository.get(userData.mUserId);
final List<InputMethodInfo> methodList = settings.getMethodList();
- int numImes = methodList.size();
+ final int numImes = methodList.size();
p.println(" Input Methods:");
for (int i = 0; i < numImes; i++) {
- InputMethodInfo info = methodList.get(i);
+ final InputMethodInfo info = methodList.get(i);
p.println(" InputMethod #" + i + ":");
info.dump(p, " ");
}
- final var bindingController = userData.mBindingController;
- p.println(" mCurMethodId=" + bindingController.getSelectedMethodId());
- client = userData.mCurClient;
- p.println(" mCurClient=" + client + " mCurSeq="
- + bindingController.getSequenceNumber());
- p.println(" mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
- userData.mImeBindingState.dump(/* prefix= */ " ", p);
- p.println(" mCurId=" + bindingController.getCurId());
- p.println(" mHaveConnection=" + bindingController.hasMainConnection());
- p.println(" mBoundToMethod=" + userData.mBoundToMethod);
- p.println(" mVisibleBound=" + bindingController.isVisibleBound());
- p.println(" mCurToken=" + bindingController.getCurToken());
- p.println(" mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId());
- p.println(" mCurHostInputToken=" + bindingController.getCurHostInputToken());
- p.println(" mCurIntent=" + bindingController.getCurIntent());
- method = bindingController.getCurMethod();
- p.println(" mCurMethod=" + method);
- p.println(" mEnabledSession=" + userData.mEnabledSession);
- final var visibilityStateComputer = userData.mVisibilityStateComputer;
- visibilityStateComputer.dump(pw, " ");
- p.println(" mInFullscreenMode=" + userData.mInFullscreenMode);
}
// Exit here for critical dump, as remaining sections require IPCs to other processes.
@@ -6172,7 +6169,7 @@
return;
}
- p.println(" ");
+ p.println("");
if (client != null) {
pw.flush();
try {
@@ -6184,25 +6181,23 @@
p.println("No input method client.");
}
synchronized (ImfLock.class) {
- if (userData.mImeBindingState.mFocusedWindowClient != null
- && client != userData.mImeBindingState.mFocusedWindowClient) {
- p.println(" ");
- p.println("Warning: Current input method client doesn't match the last focused. "
- + "window.");
+ final var focusedWindowClient = userData.mImeBindingState.mFocusedWindowClient;
+ if (focusedWindowClient != null && client != focusedWindowClient) {
+ p.println("");
+ p.println("Warning: Current input method client doesn't match the last focused"
+ + " window.");
p.println("Dumping input method client in the last focused window just in case.");
- p.println(" ");
+ p.println("");
pw.flush();
try {
- TransferPipe.dumpAsync(
- userData.mImeBindingState.mFocusedWindowClient.mClient.asBinder(), fd,
- args);
+ TransferPipe.dumpAsync(focusedWindowClient.mClient.asBinder(), fd, args);
} catch (IOException | RemoteException e) {
p.println("Failed to dump input method client in focused window: " + e);
}
}
}
- p.println(" ");
+ p.println("");
if (method != null) {
pw.flush();
try {
@@ -6215,56 +6210,51 @@
}
}
- private void dumpClientStates(Printer p) {
- p.println(" ClientStates:");
+ private void dumpClientController(@NonNull Printer p) {
+ p.println(" mClientController:");
// TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
@SuppressWarnings("GuardedBy") Consumer<ClientState> clientControllerDump = c -> {
- p.println(" " + c + ":");
- p.println(" client=" + c.mClient);
- p.println(" fallbackInputConnection="
- + c.mFallbackInputConnection);
- p.println(" sessionRequested="
- + c.mSessionRequested);
- p.println(" sessionRequestedForAccessibility="
+ p.println(" " + c + ":");
+ p.println(" client=" + c.mClient);
+ p.println(" fallbackInputConnection=" + c.mFallbackInputConnection);
+ p.println(" sessionRequested=" + c.mSessionRequested);
+ p.println(" sessionRequestedForAccessibility="
+ c.mSessionRequestedForAccessibility);
- p.println(" curSession=" + c.mCurSession);
- p.println(" selfReportedDisplayId=" + c.mSelfReportedDisplayId);
- p.println(" uid=" + c.mUid);
- p.println(" pid=" + c.mPid);
+ p.println(" curSession=" + c.mCurSession);
+ p.println(" selfReportedDisplayId=" + c.mSelfReportedDisplayId);
+ p.println(" uid=" + c.mUid);
+ p.println(" pid=" + c.mPid);
};
synchronized (ImfLock.class) {
mClientController.forAllClients(clientControllerDump);
}
}
- private void dumpUserRepository(Printer p) {
- p.println(" mUserDataRepository=");
+ private void dumpUserRepository(@NonNull Printer p) {
+ p.println(" mUserDataRepository:");
// TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
- @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump =
- u -> {
- p.println(" mUserId=" + u.mUserId);
- p.println(" unlocked=" + u.mIsUnlockingOrUnlocked.get());
- p.println(" hasMainConnection="
- + u.mBindingController.hasMainConnection());
- p.println(" isVisibleBound=" + u.mBindingController.isVisibleBound());
- p.println(" boundToMethod=" + u.mBoundToMethod);
- p.println(" curClient=" + u.mCurClient);
- if (u.mCurEditorInfo != null) {
- p.println(" curEditorInfo:");
- u.mCurEditorInfo.dump(p, " ", false /* dumpExtras */);
- } else {
- p.println(" curEditorInfo: null");
- }
- p.println(" imeBindingState:");
- u.mImeBindingState.dump(" ", p);
- p.println(" enabledSession=" + u.mEnabledSession);
- p.println(" inFullscreenMode=" + u.mInFullscreenMode);
- p.println(" imeDrawsNavBar=" + u.mImeDrawsNavBar.get());
- p.println(" switchingController:");
- u.mSwitchingController.dump(p, " ");
- p.println(" mLastEnabledInputMethodsStr="
- + u.mLastEnabledInputMethodsStr);
- };
+ @SuppressWarnings("GuardedBy") Consumer<UserData> userDataDump = u -> {
+ p.println(" userId=" + u.mUserId);
+ p.println(" unlocked=" + u.mIsUnlockingOrUnlocked.get());
+ p.println(" hasMainConnection=" + u.mBindingController.hasMainConnection());
+ p.println(" isVisibleBound=" + u.mBindingController.isVisibleBound());
+ p.println(" boundToMethod=" + u.mBoundToMethod);
+ p.println(" curClient=" + u.mCurClient);
+ if (u.mCurEditorInfo != null) {
+ p.println(" curEditorInfo:");
+ u.mCurEditorInfo.dump(p, " ", false /* dumpExtras */);
+ } else {
+ p.println(" curEditorInfo: null");
+ }
+ p.println(" imeBindingState:");
+ u.mImeBindingState.dump(p, " ");
+ p.println(" enabledSession=" + u.mEnabledSession);
+ p.println(" inFullscreenMode=" + u.mInFullscreenMode);
+ p.println(" imeDrawsNavBar=" + u.mImeDrawsNavBar.get());
+ p.println(" switchingController:");
+ u.mSwitchingController.dump(p, " ");
+ p.println(" mLastEnabledInputMethodsStr=" + u.mLastEnabledInputMethodsStr);
+ };
synchronized (ImfLock.class) {
mUserDataRepository.forAllUserData(userDataDump);
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
index b5ee068..248fa60 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
@@ -287,10 +287,10 @@
void dump(@NonNull Printer pw, @NonNull String prefix) {
final boolean showing = isisInputMethodPickerShownForTestLocked();
- pw.println(prefix + " isShowing: " + showing);
+ pw.println(prefix + "isShowing: " + showing);
if (showing) {
- pw.println(prefix + " imList: " + mImList);
+ pw.println(prefix + "imList: " + mImList);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
index 1d0e3c6..6abd5aa 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -187,10 +187,10 @@
void dump(@NonNull Printer pw, @NonNull String prefix) {
final boolean showing = isShowing();
- pw.println(prefix + " isShowing: " + showing);
+ pw.println(prefix + "isShowing: " + showing);
if (showing) {
- pw.println(prefix + " menuItems: " + mMenuItems);
+ pw.println(prefix + "menuItems: " + mMenuItems);
}
}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 19ac1ec..58b1e49 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -71,6 +71,7 @@
import android.app.admin.DevicePolicyManagerInternal;
import android.companion.virtual.VirtualDeviceManager;
import android.content.ComponentName;
+import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -649,11 +650,11 @@
int userId, int callingUid, int callingPid,
boolean includeInstantApps, boolean resolveForStart) {
if (!mUserManager.exists(userId)) return Collections.emptyList();
- enforceCrossUserOrProfilePermission(callingUid,
+ enforceCrossUserOrProfilePermission(Binder.getCallingUid(),
userId,
false /*requireFullPermission*/,
false /*checkShell*/,
- "query intent receivers");
+ "query intent services");
final String instantAppPkgName = getInstantAppPackageName(callingUid);
flags = updateFlagsForResolve(flags, userId, callingUid, includeInstantApps,
false /* isImplicitImageCaptureIntentAndNotSetByDpc */);
@@ -2208,10 +2209,10 @@
return true;
}
boolean permissionGranted = requireFullPermission ? hasPermission(
- Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
: (hasPermission(
- android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
- || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS));
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+ || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS, callingUid));
if (!permissionGranted) {
if (Process.isIsolatedUid(callingUid) && isKnownIsolatedComputeApp(callingUid)) {
return checkIsolatedOwnerHasPermission(callingUid, requireFullPermission);
@@ -4669,7 +4670,7 @@
if (!forceAllowCrossUser) {
enforceCrossUserPermission(
- callingUid,
+ Binder.getCallingUid(),
userId,
false /* requireFullPermission */,
false /* checkShell */,
@@ -4752,8 +4753,14 @@
int callingUid) {
if (!mUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId);
- final ProviderInfo providerInfo = mComponentResolver.queryProvider(this, name, flags,
- userId);
+
+ // Callers of this API may not always separate the userID and authority. Let's parse it
+ // before resolving
+ String authorityWithoutUserId = ContentProvider.getAuthorityWithoutUserId(name);
+ userId = ContentProvider.getUserIdFromAuthority(name, userId);
+
+ final ProviderInfo providerInfo = mComponentResolver.queryProvider(this,
+ authorityWithoutUserId, flags, userId);
boolean checkedGrants = false;
if (providerInfo != null) {
// Looking for cross-user grants before enforcing the typical cross-users permissions
@@ -4767,7 +4774,7 @@
if (!checkedGrants) {
boolean enforceCrossUser = true;
- if (isAuthorityRedirectedForCloneProfile(name)) {
+ if (isAuthorityRedirectedForCloneProfile(authorityWithoutUserId)) {
final UserManagerInternal umInternal = mInjector.getUserManagerInternal();
UserInfo userInfo = umInternal.getUserInfo(UserHandle.getUserId(callingUid));
@@ -5242,7 +5249,7 @@
@Override
public int getComponentEnabledSetting(@NonNull ComponentName component, int callingUid,
@UserIdInt int userId) {
- enforceCrossUserPermission(callingUid, userId, false /*requireFullPermission*/,
+ enforceCrossUserPermission(Binder.getCallingUid(), userId, false /*requireFullPermission*/,
false /*checkShell*/, "getComponentEnabled");
return getComponentEnabledSettingInternal(component, callingUid, userId);
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java b/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
index ea37d8e..99eac37 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerHistoricalSession.java
@@ -79,6 +79,8 @@
private final String mSessionErrorMessage;
private final String mPreVerifiedDomains;
private final String mPackageName;
+ private final int mInitialVerificationPolicy;
+ private final int mCurrentVerificationPolicy;
PackageInstallerHistoricalSession(int sessionId, int userId, int originalInstallerUid,
String originalInstallerPackageName, InstallSource installSource, int installerUid,
@@ -90,7 +92,8 @@
int[] childSessionIds, boolean sessionApplied, boolean sessionFailed,
boolean sessionReady, int sessionErrorCode, String sessionErrorMessage,
PreapprovalDetails preapprovalDetails, DomainSet preVerifiedDomains,
- String packageNameFromApk) {
+ String packageNameFromApk, int initialVerificationPolicy,
+ int currentVerificationPolicy) {
this.sessionId = sessionId;
this.userId = userId;
this.mOriginalInstallerUid = originalInstallerUid;
@@ -140,6 +143,8 @@
this.mPackageName = preapprovalDetails != null ? preapprovalDetails.getPackageName()
: packageNameFromApk != null ? packageNameFromApk : params.appPackageName;
+ this.mInitialVerificationPolicy = initialVerificationPolicy;
+ this.mCurrentVerificationPolicy = currentVerificationPolicy;
}
void dump(IndentingPrintWriter pw) {
@@ -184,6 +189,8 @@
pw.printPair("mPreapprovalDetails", mPreapprovalDetails);
pw.printPair("mPreVerifiedDomains", mPreVerifiedDomains);
pw.printPair("mAppPackageName", mPackageName);
+ pw.printPair("mInitialVerificationPolicy", mInitialVerificationPolicy);
+ pw.printPair("mCurrentVerificationPolicy", mCurrentVerificationPolicy);
pw.println();
pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index a59f4bd..ef09976 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -30,6 +30,7 @@
import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT;
import static android.os.Process.INVALID_UID;
import static android.os.Process.SYSTEM_UID;
+import static android.os.UserHandle.USER_SYSTEM;
import static com.android.server.pm.PackageArchiver.isArchivingEnabled;
import static com.android.server.pm.PackageInstallerSession.isValidVerificationPolicy;
@@ -154,7 +155,6 @@
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntPredicate;
import java.util.function.Supplier;
@@ -281,11 +281,12 @@
};
/**
- * Default verification policy for incoming installation sessions.
- * TODO(b/360129657): update the default policy.
+ * Default verification policy for incoming installation sessions, mapped from userId to policy.
*/
- private final AtomicInteger mVerificationPolicy = new AtomicInteger(
- VERIFICATION_POLICY_BLOCK_FAIL_WARN);
+ @GuardedBy("mVerificationPolicyPerUser")
+ private final SparseIntArray mVerificationPolicyPerUser = new SparseIntArray(1);
+ // TODO(b/360129657): update the default policy.
+ private static final int DEFAULT_VERIFICATION_POLICY = VERIFICATION_POLICY_BLOCK_FAIL_WARN;
private static final class Lifecycle extends SystemService {
private final PackageInstallerService mPackageInstallerService;
@@ -342,6 +343,9 @@
context, mInstallThread.getLooper(), new AppStateHelper(context));
mPackageArchiver = new PackageArchiver(mContext, mPm);
mVerifierController = new VerifierController(mContext, mInstallHandler);
+ synchronized (mVerificationPolicyPerUser) {
+ mVerificationPolicyPerUser.put(USER_SYSTEM, DEFAULT_VERIFICATION_POLICY);
+ }
LocalServices.getService(SystemServiceManager.class).startService(
new Lifecycle(context, this));
@@ -1051,12 +1055,17 @@
InstallSource installSource = InstallSource.create(installerPackageName,
originatingPackageName, requestedInstallerPackageName, requestedInstallerPackageUid,
requestedInstallerPackageName, installerAttributionTag, params.packageSource);
+ final int verificationPolicy;
+ synchronized (mVerificationPolicyPerUser) {
+ verificationPolicy = mVerificationPolicyPerUser.get(
+ userId, DEFAULT_VERIFICATION_POLICY);
+ }
session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this,
mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId,
userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
false, false, false, PackageManager.INSTALL_UNKNOWN, "", null,
- mVerifierController, mVerificationPolicy.get());
+ mVerifierController, verificationPolicy, verificationPolicy);
synchronized (mSessions) {
mSessions.put(sessionId, session);
@@ -1882,14 +1891,22 @@
@Override
@EnforcePermission(android.Manifest.permission.VERIFICATION_AGENT)
- public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
+ public @PackageInstaller.VerificationPolicy int getVerificationPolicy(int userId) {
getVerificationPolicy_enforcePermission();
- return mVerificationPolicy.get();
+ synchronized (mVerificationPolicyPerUser) {
+ if (mVerificationPolicyPerUser.indexOfKey(userId) < 0) {
+ throw new IllegalStateException(
+ "Verification policy for user " + userId + " does not exist."
+ + " Does the user exist?");
+ }
+ return mVerificationPolicyPerUser.get(userId);
+ }
}
@Override
@EnforcePermission(android.Manifest.permission.VERIFICATION_AGENT)
- public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy) {
+ public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy,
+ int userId) {
setVerificationPolicy_enforcePermission();
final int callingUid = getCallingUid();
// Only the verifier currently bound by the system can change the policy, except for Shell
@@ -1899,12 +1916,31 @@
if (!isValidVerificationPolicy(policy)) {
return false;
}
- if (policy != mVerificationPolicy.get()) {
- mVerificationPolicy.set(policy);
+ synchronized (mVerificationPolicyPerUser) {
+ if (mVerificationPolicyPerUser.indexOfKey(userId) < 0) {
+ throw new IllegalStateException(
+ "Verification policy for user " + userId + " does not exist."
+ + " Does the user exist?");
+ }
+ if (policy != mVerificationPolicyPerUser.get(userId)) {
+ mVerificationPolicyPerUser.put(userId, policy);
+ }
}
return true;
}
+ void onUserAdded(int userId) {
+ synchronized (mVerificationPolicyPerUser) {
+ mVerificationPolicyPerUser.put(userId, DEFAULT_VERIFICATION_POLICY);
+ }
+ }
+
+ void onUserRemoved(int userId) {
+ synchronized (mVerificationPolicyPerUser) {
+ mVerificationPolicyPerUser.delete(userId);
+ }
+ }
+
private static int getSessionCount(SparseArray<PackageInstallerSession> sessions,
int installerUid) {
int count = 0;
@@ -2301,6 +2337,9 @@
}
mSilentUpdatePolicy.dump(pw);
mGentleUpdateHelper.dump(pw);
+ synchronized (mVerificationPolicyPerUser) {
+ pw.printPair("VerificationPolicyPerUser", mVerificationPolicyPerUser.toString());
+ }
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 6ea5369..2a92de5 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -320,7 +320,8 @@
private static final String ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT =
"applicationEnabledSettingPersistent";
private static final String ATTR_DOMAIN = "domain";
- private static final String ATTR_VERIFICATION_POLICY = "verificationPolicy";
+ private static final String ATTR_INITIAL_VERIFICATION_POLICY = "initialVerificationPolicy";
+ private static final String ATTR_CURRENT_VERIFICATION_POLICY = "currentVerificationPolicy";
private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill";
private static final int[] EMPTY_CHILD_SESSION_ARRAY = EmptyArray.INT;
@@ -418,10 +419,14 @@
private final PackageSessionProvider mSessionProvider;
private final SilentUpdatePolicy mSilentUpdatePolicy;
/**
- * The verification policy applied to this session, which might be different from the default
- * verification policy used by the system.
+ * The initial verification policy assigned to this session when it was first created.
*/
- private final AtomicInteger mVerificationPolicy;
+ private final int mInitialVerificationPolicy;
+ /**
+ * The active verification policy, which might be different from the initial verification policy
+ * assigned to this session or the default policy currently used by the system.
+ */
+ private final AtomicInteger mCurrentVerificationPolicy;
/**
* Note all calls must be done outside {@link #mLock} to prevent lock inversion.
*/
@@ -1182,7 +1187,8 @@
boolean isFailed, boolean isApplied, int sessionErrorCode,
String sessionErrorMessage, DomainSet preVerifiedDomains,
@NonNull VerifierController verifierController,
- @PackageInstaller.VerificationPolicy int verificationPolicy) {
+ @PackageInstaller.VerificationPolicy int initialVerificationPolicy,
+ @PackageInstaller.VerificationPolicy int currentVerificationPolicy) {
mCallback = callback;
mContext = context;
mPm = pm;
@@ -1192,7 +1198,8 @@
mHandler = new Handler(looper, mHandlerCallback);
mStagingManager = stagingManager;
mVerifierController = verifierController;
- mVerificationPolicy = new AtomicInteger(verificationPolicy);
+ mInitialVerificationPolicy = initialVerificationPolicy;
+ mCurrentVerificationPolicy = new AtomicInteger(currentVerificationPolicy);
this.sessionId = sessionId;
this.userId = userId;
@@ -1302,7 +1309,8 @@
mStageDirInUse, mDestroyed, mFds.size(), mBridges.size(), mFinalStatus,
mFinalMessage, params, mParentSessionId, getChildSessionIdsLocked(),
mSessionApplied, mSessionFailed, mSessionReady, mSessionErrorCode,
- mSessionErrorMessage, mPreapprovalDetails, mPreVerifiedDomains, mPackageName);
+ mSessionErrorMessage, mPreapprovalDetails, mPreVerifiedDomains, mPackageName,
+ mInitialVerificationPolicy, mCurrentVerificationPolicy.get());
}
}
@@ -2887,8 +2895,8 @@
final VerifierCallback verifierCallback = new VerifierCallback();
if (!mVerifierController.startVerificationSession(mPm::snapshotComputer, userId,
sessionId, getPackageName(), Uri.fromFile(stageDir), signingInfo,
- declaredLibraries, mVerificationPolicy.get(), /* extensionParams= */ null,
- verifierCallback, /* retry= */ false)) {
+ declaredLibraries, mCurrentVerificationPolicy.get(),
+ /* extensionParams= */ null, verifierCallback, /* retry= */ false)) {
// A verifier is installed but cannot be connected.
verifierCallback.onConnectionFailed();
}
@@ -2967,7 +2975,7 @@
* verification policy for this session.
*/
public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
- return mVerificationPolicy.get();
+ return mCurrentVerificationPolicy.get();
}
/**
* Called by the VerifierController when the verifier requests to change the verification
@@ -2977,7 +2985,7 @@
if (!isValidVerificationPolicy(policy)) {
return false;
}
- mVerificationPolicy.set(policy);
+ mCurrentVerificationPolicy.set(policy);
return true;
}
/**
@@ -3023,7 +3031,7 @@
// TODO: handle extension response
mHandler.post(() -> {
if (statusReceived.isVerified()
- || mVerificationPolicy.get() == VERIFICATION_POLICY_NONE) {
+ || mCurrentVerificationPolicy.get() == VERIFICATION_POLICY_NONE) {
// Continue with the rest of the verification and installation.
resumeVerify();
return;
@@ -3067,13 +3075,14 @@
}
private void handleNonPackageBlockedFailure(Runnable onFailWarning, Runnable onFailClosed) {
- final Runnable r = switch (mVerificationPolicy.get()) {
+ final Runnable r = switch (mCurrentVerificationPolicy.get()) {
case VERIFICATION_POLICY_NONE, VERIFICATION_POLICY_BLOCK_FAIL_OPEN ->
PackageInstallerSession.this::resumeVerify;
case VERIFICATION_POLICY_BLOCK_FAIL_WARN -> onFailWarning;
case VERIFICATION_POLICY_BLOCK_FAIL_CLOSED -> onFailClosed;
default -> {
- Log.wtf(TAG, "Unknown verification policy: " + mVerificationPolicy.get());
+ Log.wtf(TAG, "Unknown verification policy: "
+ + mCurrentVerificationPolicy.get());
yield onFailClosed;
}
};
@@ -5436,12 +5445,21 @@
}
/**
+ * @return the initial policy for the verification request assigned to the session when created.
+ */
+ @VisibleForTesting
+ public @PackageInstaller.VerificationPolicy int getInitialVerificationPolicy() {
+ assertCallerIsOwnerOrRoot();
+ return mInitialVerificationPolicy;
+ }
+
+ /**
* @return the current policy for the verification request associated with this session.
*/
@VisibleForTesting
- public @PackageInstaller.VerificationPolicy int getVerificationPolicy() {
+ public @PackageInstaller.VerificationPolicy int getCurrentVerificationPolicy() {
assertCallerIsOwnerOrRoot();
- return mVerificationPolicy.get();
+ return mCurrentVerificationPolicy.get();
}
void setSessionReady() {
@@ -5681,6 +5699,8 @@
if (mPreVerifiedDomains != null) {
pw.printPair("mPreVerifiedDomains", mPreVerifiedDomains);
}
+ pw.printPair("mInitialVerificationPolicy", mInitialVerificationPolicy);
+ pw.printPair("mCurrentVerificationPolicy", mCurrentVerificationPolicy.get());
pw.println();
pw.decreaseIndent();
@@ -5905,7 +5925,9 @@
out.attributeInt(null, ATTR_INSTALL_REASON, params.installReason);
writeBooleanAttribute(out, ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT,
params.applicationEnabledSettingPersistent);
- out.attributeInt(null, ATTR_VERIFICATION_POLICY, mVerificationPolicy.get());
+ out.attributeInt(null, ATTR_INITIAL_VERIFICATION_POLICY, mInitialVerificationPolicy);
+ out.attributeInt(null, ATTR_CURRENT_VERIFICATION_POLICY,
+ mCurrentVerificationPolicy.get());
final boolean isDataLoader = params.dataLoaderParams != null;
writeBooleanAttribute(out, ATTR_IS_DATALOADER, isDataLoader);
@@ -6056,8 +6078,10 @@
final boolean sealed = in.getAttributeBoolean(null, ATTR_SEALED, false);
final int parentSessionId = in.getAttributeInt(null, ATTR_PARENT_SESSION_ID,
SessionInfo.INVALID_ID);
- final int verificationPolicy = in.getAttributeInt(null, ATTR_VERIFICATION_POLICY,
- VERIFICATION_POLICY_NONE);
+ final int initialVerificationPolicy = in.getAttributeInt(null,
+ ATTR_INITIAL_VERIFICATION_POLICY, VERIFICATION_POLICY_NONE);
+ final int currentVerificationPolicy = in.getAttributeInt(null,
+ ATTR_CURRENT_VERIFICATION_POLICY, VERIFICATION_POLICY_NONE);
final SessionParams params = new SessionParams(
SessionParams.MODE_INVALID);
@@ -6233,6 +6257,6 @@
stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed,
childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied,
sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController,
- verificationPolicy);
+ initialVerificationPolicy, currentVerificationPolicy);
}
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 807445e..9d48efe9 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4421,6 +4421,7 @@
mPendingBroadcasts.remove(userId);
mAppsFilter.onUserDeleted(snapshotComputer(), userId);
mPermissionManager.onUserRemoved(userId);
+ mInstallerService.onUserRemoved(userId);
}
mInstantAppRegistry.onUserRemoved(userId);
mPackageMonitorCallbackHelper.onUserRemoved(userId);
@@ -4471,6 +4472,7 @@
mLegacyPermissionManager.grantDefaultPermissions(userId);
mPermissionManager.setDefaultPermissionGrantFingerprint(Build.FINGERPRINT, userId);
mDomainVerificationManager.clearUser(userId);
+ mInstallerService.onUserAdded(userId);
}
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index f8e56e1..7ef3582 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -4676,7 +4676,7 @@
try {
final IPackageInstaller installer = mInterface.getPackageInstaller();
// TODO(b/360129657): global verification policy should be per user
- final int policy = installer.getVerificationPolicy();
+ final int policy = installer.getVerificationPolicy(translatedUserId);
pw.println(policy);
} catch (Exception e) {
pw.println("Failure [" + e.getMessage() + "]");
@@ -4717,7 +4717,8 @@
try {
final IPackageInstaller installer = mInterface.getPackageInstaller();
// TODO(b/360129657): global verification policy should be per user
- final boolean success = installer.setVerificationPolicy(Integer.parseInt(policyStr));
+ final boolean success = installer.setVerificationPolicy(Integer.parseInt(policyStr),
+ translatedUserId);
if (!success) {
pw.println("Failure setting verification policy.");
return 1;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 6baab25..7eb0c42 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -28,6 +28,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.server.utils.TimingsTraceAndSlog.SYSTEM_SERVER_TIMING_TAG;
+import static com.android.tradeinmode.flags.Flags.enableTradeInMode;
import android.annotation.NonNull;
import android.annotation.StringRes;
@@ -1399,10 +1400,6 @@
mSystemServiceManager.startService(BatteryService.class);
t.traceEnd();
- t.traceBegin("StartTradeInModeService");
- mSystemServiceManager.startService(TradeInModeService.class);
- t.traceEnd();
-
// Tracks application usage stats.
t.traceBegin("StartUsageService");
mSystemServiceManager.startService(UsageStatsService.class);
@@ -1772,6 +1769,13 @@
mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class);
t.traceEnd();
}
+
+ if (!isWatch && !isTv && !isAutomotive && enableTradeInMode()) {
+ t.traceBegin("StartTradeInModeService");
+ mSystemServiceManager.startService(TradeInModeService.class);
+ t.traceEnd();
+ }
+
} catch (Throwable e) {
Slog.e("System", "******************************************");
Slog.e("System", "************ Failure starting core service");
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 2bc8af1..5bb6b19 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -85,6 +85,8 @@
private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
"settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
private Instrumentation mInstrumentation;
private UiDevice mUiDevice;
private Context mContext;
@@ -95,7 +97,7 @@
private boolean mShowImeWithHardKeyboardEnabled;
@Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
@Before
public void setUp() throws Exception {
@@ -159,7 +161,7 @@
// Press home key to hide soft keyboard.
Log.i(TAG, "Press home");
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
assertThat(mUiDevice.pressHome()).isTrue();
// The IME visibility is only sent at the end of the animation. Therefore, we have to
// wait until the visibility was sent to the server and the IME window hidden.
@@ -774,7 +776,7 @@
backButtonUiObject.click();
mInstrumentation.waitForIdleSync();
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
// The IME visibility is only sent at the end of the animation. Therefore, we have to
// wait until the visibility was sent to the server and the IME window hidden.
eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
@@ -812,7 +814,7 @@
backButtonUiObject.longClick();
mInstrumentation.waitForIdleSync();
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
// The IME visibility is only sent at the end of the animation. Therefore, we have to
// wait until the visibility was sent to the server and the IME window hidden.
eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
@@ -900,7 +902,7 @@
assertWithMessage("Input Method Switcher Menu is shown")
.that(isInputMethodPickerShown(imm))
.isTrue();
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
// The IME visibility is only sent at the end of the animation. Therefore, we have to
// wait until the visibility was sent to the server and the IME window hidden.
eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 6afcae7..3aeab09 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -76,8 +76,10 @@
@RunWith(AndroidJUnit4.class)
public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTestBase {
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
@Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider);
private DefaultImeVisibilityApplier mVisibilityApplier;
@Before
@@ -151,7 +153,7 @@
mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
STATE_HIDE_IME_EXPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId);
}
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
verifySetImeVisibility(false /* setVisible */, true /* invoked */);
} else {
@@ -168,7 +170,7 @@
mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
STATE_HIDE_IME_NOT_ALWAYS, eq(SoftInputShowHideReason.NOT_SET), mUserId);
}
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
verifySetImeVisibility(false /* setVisible */, true /* invoked */);
} else {
@@ -182,7 +184,7 @@
mVisibilityApplier.applyImeVisibility(mWindowToken, ImeTracker.Token.empty(),
STATE_SHOW_IME_IMPLICIT, eq(SoftInputShowHideReason.NOT_SET), mUserId);
}
- if (Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, true /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -260,7 +262,7 @@
verify(mVisibilityApplier).applyImeVisibility(
eq(mWindowToken), any(), eq(STATE_HIDE_IME),
eq(SoftInputShowHideReason.NOT_SET), eq(mUserId) /* userId */);
- if (!Flags.refactorInsetsController()) {
+ if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verify(mInputMethodManagerService.mWindowManagerInternal).hideIme(eq(mWindowToken),
eq(displayIdToShowIme), and(not(eq(statsToken)), notNull()));
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index 4d956b2..c958bd3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -39,8 +39,10 @@
import android.os.IBinder;
import android.os.LocaleList;
import android.os.RemoteException;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.Flags;
import android.window.ImeOnBackInvokedDispatcher;
import com.android.internal.inputmethod.IInputMethodClient;
@@ -89,6 +91,9 @@
};
private static final int DEFAULT_SOFT_INPUT_FLAG =
StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR;
+
+ private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider();
+
@Mock
VirtualDeviceManagerInternal mMockVdmInternal;
@@ -125,7 +130,7 @@
case SOFT_INPUT_STATE_UNSPECIFIED:
boolean showSoftInput =
(mSoftInputAdjustment == SOFT_INPUT_ADJUST_RESIZE) || mIsLargeScreen;
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, showSoftInput /* invoked */);
// A hide can only be triggered if there is no editorFocused, which this test
// always sets.
@@ -141,7 +146,7 @@
break;
case SOFT_INPUT_STATE_VISIBLE:
case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, true /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -150,7 +155,7 @@
}
break;
case SOFT_INPUT_STATE_UNCHANGED:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -160,7 +165,7 @@
break;
case SOFT_INPUT_STATE_HIDDEN:
case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
// In this case, we don't have to manipulate the requested visible types of
// the WindowState, as they're already in the correct state
@@ -192,7 +197,7 @@
case SOFT_INPUT_STATE_UNSPECIFIED:
boolean hideSoftInput =
(mSoftInputAdjustment != SOFT_INPUT_ADJUST_RESIZE) && !mIsLargeScreen;
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
// A show can only be triggered in forward navigation
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
// A hide can only be triggered if there is no editorFocused, which this test
@@ -209,7 +214,7 @@
case SOFT_INPUT_STATE_VISIBLE:
case SOFT_INPUT_STATE_HIDDEN:
case SOFT_INPUT_STATE_UNCHANGED: // Do nothing
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -218,7 +223,7 @@
}
break;
case SOFT_INPUT_STATE_ALWAYS_VISIBLE:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, true /* invoked */);
verifySetImeVisibility(false /* setVisible */, false /* invoked */);
} else {
@@ -227,7 +232,7 @@
}
break;
case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
- if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) {
verifySetImeVisibility(true /* setVisible */, false /* invoked */);
// In this case, we don't have to manipulate the requested visible types of
// the WindowState, as they're already in the correct state
diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
index 5c4716d..7d5532f 100644
--- a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
+++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
@@ -57,6 +57,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doReturn
@@ -383,6 +384,10 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
PackageManager.PERMISSION_GRANTED
}
+ whenever(this.checkPermission(
+ eq(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL), anyInt(), anyInt())) {
+ PackageManager.PERMISSION_GRANTED
+ }
}
val mockSharedLibrariesImpl: SharedLibrariesImpl = mock {
whenever(this.snapshot()) { this@mock }
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
index 1e89359..09d0e4a 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
@@ -22,6 +22,7 @@
import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_DENIED
import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_GRANTED
import android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
+import android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_OPEN
import android.content.pm.PackageManager
import android.content.pm.verify.domain.DomainSet
import android.os.Parcel
@@ -199,7 +200,8 @@
/* stagedSessionErrorMessage */ "some error",
/* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")),
/* VerifierController */ mock(VerifierController::class.java),
- VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
+ /* initialVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_OPEN,
+ /* currentVerificationPolicy */ VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
)
}
@@ -342,7 +344,8 @@
assertThat(expected.childSessionIds).asList()
.containsExactlyElementsIn(actual.childSessionIds.toList())
assertThat(expected.preVerifiedDomains).isEqualTo(actual.preVerifiedDomains)
- assertThat(expected.verificationPolicy).isEqualTo(actual.verificationPolicy)
+ assertThat(expected.initialVerificationPolicy).isEqualTo(actual.initialVerificationPolicy)
+ assertThat(expected.currentVerificationPolicy).isEqualTo(actual.currentVerificationPolicy)
}
private fun assertInstallSourcesEquivalent(expected: InstallSource, actual: InstallSource) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
index 124c41e..591e8df 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
@@ -742,7 +742,10 @@
/* stagedSessionErrorMessage */ "no error",
/* preVerifiedDomains */ null,
/* verifierController */ null,
- /* verificationPolicy */ PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED);
+ /* initialVerificationPolicy */
+ PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED,
+ /* currentVerificationPolicy */
+ PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED);
StagingManager.StagedSession stagedSession = spy(session.mStagedSession);
doReturn(packageName).when(stagedSession).getPackageName();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
index 5de323b..4d82c3c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
@@ -209,18 +209,30 @@
}
@Test
- public void getShortDaysSummary_onlyDays() {
+ public void getDaysOfWeekShort_summarizesDays() {
ScheduleInfo scheduleInfo = new ScheduleInfo();
scheduleInfo.startHour = 10;
scheduleInfo.endHour = 16;
scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
- assertThat(SystemZenRules.getShortDaysSummary(mContext, scheduleInfo))
+ assertThat(SystemZenRules.getDaysOfWeekShort(mContext, scheduleInfo))
.isEqualTo("Mon-Fri");
}
@Test
+ public void getDaysOfWeekFull_summarizesDays() {
+ ScheduleInfo scheduleInfo = new ScheduleInfo();
+ scheduleInfo.startHour = 10;
+ scheduleInfo.endHour = 16;
+ scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
+ Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
+
+ assertThat(SystemZenRules.getDaysOfWeekFull(mContext, scheduleInfo))
+ .isEqualTo("Monday to Friday");
+ }
+
+ @Test
public void getTimeSummary_onlyTime() {
ScheduleInfo scheduleInfo = new ScheduleInfo();
scheduleInfo.startHour = 11;
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 4ac567c..1c6bd11 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -109,11 +109,7 @@
if (motionEventHelper.inputMethod == TOUCH
&& Flags.enableHoldToDragAppHandle()) {
// Touch requires hold-to-drag.
- val downTime = SystemClock.uptimeMillis()
- motionEventHelper.actionDown(startX, startY, time = downTime)
- SystemClock.sleep(100L) // hold for 100ns before starting the move.
- motionEventHelper.actionMove(startX, startY, startX, endY, 100, downTime = downTime)
- motionEventHelper.actionUp(startX, endY, downTime = downTime)
+ motionEventHelper.holdToDrag(startX, startY, startX, endY, steps = 100)
} else {
device.drag(startX, startY, startX, endY, 100)
}
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
index 86a0b0f..1fe6088 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt
@@ -54,7 +54,15 @@
injectMotionEvent(ACTION_UP, x, y, downTime = downTime)
}
- fun actionMove(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int, downTime: Long) {
+ fun actionMove(
+ startX: Int,
+ startY: Int,
+ endX: Int,
+ endY: Int,
+ steps: Int,
+ downTime: Long,
+ withMotionEventInjectDelay: Boolean = false
+ ) {
val incrementX = (endX - startX).toFloat() / (steps - 1)
val incrementY = (endY - startY).toFloat() / (steps - 1)
@@ -65,9 +73,33 @@
val moveEvent = getMotionEvent(downTime, time, ACTION_MOVE, x, y)
injectMotionEvent(moveEvent)
+ if (withMotionEventInjectDelay) {
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS)
+ }
}
}
+ /**
+ * Drag from [startX], [startY] to [endX], [endY] with a "hold" period after touching down
+ * and before moving.
+ */
+ fun holdToDrag(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int) {
+ val downTime = SystemClock.uptimeMillis()
+ actionDown(startX, startY, time = downTime)
+ SystemClock.sleep(100L) // Hold before dragging.
+ actionMove(
+ startX,
+ startY,
+ endX,
+ endY,
+ steps,
+ downTime,
+ withMotionEventInjectDelay = true
+ )
+ SystemClock.sleep(REGULAR_CLICK_LENGTH)
+ actionUp(startX, endX, downTime)
+ }
+
private fun injectMotionEvent(
action: Int,
x: Int,
@@ -120,4 +152,9 @@
event.displayId = 0
return event
}
+
+ companion object {
+ private const val MOTION_EVENT_INJECTION_DELAY_MILLIS = 5L
+ private const val REGULAR_CLICK_LENGTH = 100L
+ }
}
\ No newline at end of file
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 498e431..232b402 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -1574,7 +1574,10 @@
// If the file path ends with .flata, .jar, .jack, or .zip the file is treated
// as ZIP archive and the files within are merged individually.
// Otherwise the file is processed on its own.
- bool MergePath(const std::string& path, bool override) {
+ bool MergePath(std::string path, bool override) {
+ if (path.size() > 2 && util::StartsWith(path, "'") && util::EndsWith(path, "'")) {
+ path = path.substr(1, path.size() - 2);
+ }
if (util::EndsWith(path, ".flata") || util::EndsWith(path, ".jar") ||
util::EndsWith(path, ".jack") || util::EndsWith(path, ".zip")) {
return MergeArchive(path, override);