Merge "[flexiglass] Add start order dep from SysuiStatusBarStateController on CentralSurfaces" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 3e5f1cb..fa11278 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -379,6 +379,7 @@
aconfig_declarations {
name: "com.android.internal.os.flags-aconfig",
package: "com.android.internal.os",
+ container: "system",
srcs: ["core/java/com/android/internal/os/flags.aconfig"],
}
@@ -853,6 +854,7 @@
aconfig_declarations {
name: "com.android.server.contextualsearch.flags-aconfig",
package: "com.android.server.contextualsearch.flags",
+ container: "system",
srcs: ["services/contextualsearch/flags/flags.aconfig"],
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 443a6c0e..5a3ff83 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -263,6 +263,7 @@
method @RequiresPermission("android.permission.MANAGE_APPOPS") public void setHistoryParameters(int, long, int);
method @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) public void setMode(int, int, String, int);
method public static int strOpToOp(@NonNull String);
+ method public int unsafeCheckOpRawNoThrow(@NonNull String, @NonNull android.content.AttributionSource);
field public static final int ATTRIBUTION_CHAIN_ID_NONE = -1; // 0xffffffff
field public static final int ATTRIBUTION_FLAGS_NONE = 0; // 0x0
field public static final int ATTRIBUTION_FLAG_ACCESSOR = 1; // 0x1
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index f4d1304..b3312a8 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -8788,6 +8788,18 @@
* Does not throw a security exception, does not translate {@link #MODE_FOREGROUND}.
* @hide
*/
+ @TestApi
+ @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+ public int unsafeCheckOpRawNoThrow(
+ @NonNull String op, @NonNull AttributionSource attributionSource) {
+ return unsafeCheckOpRawNoThrow(strOpToOp(op), attributionSource);
+ }
+
+ /**
+ * Returns the <em>raw</em> mode associated with the op.
+ * Does not throw a security exception, does not translate {@link #MODE_FOREGROUND}.
+ * @hide
+ */
public int unsafeCheckOpRawNoThrow(int op, int uid, @NonNull String packageName) {
return unsafeCheckOpRawNoThrow(op, uid, packageName, Context.DEVICE_ID_DEFAULT);
}
@@ -8798,8 +8810,8 @@
if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
return mService.checkOperationRaw(op, uid, packageName, null);
} else {
- return mService.checkOperationRawForDevice(op, uid, packageName, null,
- Context.DEVICE_ID_DEFAULT);
+ return mService.checkOperationRawForDevice(
+ op, uid, packageName, null, virtualDeviceId);
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 436221f..714454b 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -93,7 +93,10 @@
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
+import android.text.style.UnderlineSpan;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -3167,9 +3170,6 @@
+ " instance is a custom Parcelable and not allowed in Notification");
return cs.toString();
}
- if (Flags.cleanUpSpansAndNewLines()) {
- return stripStyling(cs);
- }
return removeTextSizeSpans(cs);
}
@@ -8285,9 +8285,6 @@
*/
public BigTextStyle bigText(CharSequence cs) {
mBigText = safeCharSequence(cs);
- if (Flags.cleanUpSpansAndNewLines()) {
- mBigText = cleanUpNewLines(mBigText);
- }
return this;
}
@@ -8358,6 +8355,9 @@
// Replace the text with the big text, but only if the big text is not empty.
CharSequence bigTextText = mBuilder.processLegacyText(mBigText);
+ if (Flags.cleanUpSpansAndNewLines()) {
+ bigTextText = cleanUpNewLines(stripStyling(bigTextText));
+ }
if (!TextUtils.isEmpty(bigTextText)) {
p.text(bigTextText);
}
@@ -9273,11 +9273,43 @@
*/
public void ensureColorContrastOrStripStyling(int backgroundColor) {
if (Flags.cleanUpSpansAndNewLines()) {
- mText = stripStyling(mText);
+ mText = stripNonStyleSpans(mText);
} else {
ensureColorContrast(backgroundColor);
}
}
+
+ private CharSequence stripNonStyleSpans(CharSequence text) {
+
+ if (text instanceof Spanned) {
+ Spanned ss = (Spanned) text;
+ Object[] spans = ss.getSpans(0, ss.length(), Object.class);
+ SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
+ for (Object span : spans) {
+ final Object resultSpan;
+ if (span instanceof StyleSpan
+ || span instanceof StrikethroughSpan
+ || span instanceof UnderlineSpan) {
+ resultSpan = span;
+ } else if (span instanceof TextAppearanceSpan) {
+ final TextAppearanceSpan originalSpan = (TextAppearanceSpan) span;
+ resultSpan = new TextAppearanceSpan(
+ null,
+ originalSpan.getTextStyle(),
+ -1,
+ null,
+ null);
+ } else {
+ continue;
+ }
+ builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
+ ss.getSpanFlags(span));
+ }
+ return builder;
+ }
+ return text;
+ }
+
/**
* Updates TextAppearance spans in the message text so it has sufficient contrast
* against its background.
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index 06a0f5c..769b658 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -237,19 +237,15 @@
@IntRange(from = 1) int height,
@ImageFormat.Format int format,
@IntRange(from = 1) int maximumFramesPerSecond) {
- // TODO(b/310857519): Check dimension upper limits based on the maximum texture size
- // supported by the current device, instead of hardcoded limits.
- if (width <= 0 || width > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+ if (width <= 0) {
throw new IllegalArgumentException(
"Invalid width passed for stream config: " + width
- + ", must be between 1 and "
- + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
+ + ", must be greater than 0");
}
- if (height <= 0 || height > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) {
+ if (height <= 0) {
throw new IllegalArgumentException(
"Invalid height passed for stream config: " + height
- + ", must be between 1 and "
- + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT);
+ + ", must be greater than 0");
}
if (!isFormatSupported(format)) {
throw new IllegalArgumentException(
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index 00a814e..6ab66b3 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -39,11 +39,6 @@
public final class VirtualCameraStreamConfig implements Parcelable {
// TODO(b/310857519): Check if we should increase the fps upper limit in future.
static final int MAX_FPS_UPPER_LIMIT = 60;
- // This is the minimum guaranteed upper bound of texture size supported by all devices.
- // Keep this in sync with kMaxTextureSize from services/camera/virtualcamera/util/Util.cc
- // TODO(b/310857519): Remove this once we add support for fetching the maximum texture size
- // supported by the current device.
- static final int DIMENSION_UPPER_LIMIT = 2048;
private final int mWidth;
private final int mHeight;
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 535cebb..7ac9547 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1548,6 +1548,26 @@
public static final long INSETS_DECOUPLED_CONFIGURATION_ENFORCED = 151861875L;
/**
+ * When enabled, the activity will receive configuration decoupled from system bar insets.
+ *
+ * <p>This will only apply if the activity is targeting SDK level 34 or earlier versions.
+ *
+ * <p>This will only in effect if the device is trying to provide a different value by default
+ * other than the legacy value, i.e., the
+ * {@code Flags.allowsScreenSizeDecoupledFromStatusBarAndCutout()} is set to true.
+ *
+ * <p>If the {@code Flags.insetsDecoupledConfiguration()} is also set to true, all apps
+ * targeting SDK level 35 or later, and apps with this override flag will receive the insets
+ * decoupled configuration.
+ *
+ * @hide
+ */
+ @ChangeId
+ @Disabled
+ @Overridable
+ public static final long OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION = 327313645L;
+
+ /**
* Optional set of a certificates identifying apps that are allowed to embed this activity. From
* the "knownActivityEmbeddingCerts" attribute.
*/
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 6d3a56d..c57a3a6 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -113,6 +113,26 @@
is_fixed_read_only: true
}
+flag {
+ name: "fix_avatar_picker_read_back_order"
+ namespace: "multiuser"
+ description: "Talkback focus doesn't move to the 'If you change your Google Account picture…' after swiping next to move the focus from 'Choose a picture'"
+ bug: "330835921"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "fix_avatar_picker_selected_read_back"
+ namespace: "multiuser"
+ description: "Talkback doesn't announce 'selected' after double tapping the button in the picture list in 'Choose a picture' page."
+ bug: "330840549"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
# This flag guards the private space feature and all its implementations excluding the APIs. APIs are guarded by android.os.Flags.allow_private_profile.
flag {
name: "enable_private_space_features"
diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java
index 7aa0349..3c4307c 100644
--- a/core/java/android/database/DatabaseUtils.java
+++ b/core/java/android/database/DatabaseUtils.java
@@ -1671,7 +1671,7 @@
return null;
}
final int eos = Math.min(n+3, end);
- return sql.substring(n, eos);
+ return sql.substring(n, eos).toUpperCase(Locale.ROOT);
}
/**
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 9a02b74b..5da0cb4 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -79,6 +79,11 @@
mService.endDream(this);
}
+ @Override
+ public void comeToFront() {
+ mService.comeToFront(this);
+ }
+
private void onExitRequested() {
try {
mDreamOverlayCallback.onExitRequested();
@@ -130,6 +135,16 @@
});
}
+ private void comeToFront(OverlayClient client) {
+ mExecutor.execute(() -> {
+ if (mCurrentClient != client) {
+ return;
+ }
+
+ onComeToFront();
+ });
+ }
+
private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
@Override
public void getClient(IDreamOverlayClientCallback callback) {
@@ -190,6 +205,13 @@
public void onWakeUp() {}
/**
+ * This method is overridden by implementations to handle when the dream is coming to the front
+ * (after having lost focus to something on top of it).
+ * @hide
+ */
+ public void onComeToFront() {}
+
+ /**
* This method is overridden by implementations to handle when the dream has ended. There may
* be earlier signals leading up to this step, such as @{@link #onWakeUp(Runnable)}.
*
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 353828c..c26d83c 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -18,6 +18,7 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.service.dreams.Flags.dreamHandlesConfirmKeys;
+import static android.service.dreams.Flags.dreamTracksFocus;
import android.annotation.FlaggedApi;
import android.annotation.IdRes;
@@ -52,6 +53,7 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.service.controls.flags.Flags;
+import android.service.dreams.utils.DreamAccessibility;
import android.util.AttributeSet;
import android.util.Log;
import android.util.MathUtils;
@@ -273,6 +275,7 @@
private boolean mDebug = false;
private ComponentName mDreamComponent;
+ private DreamAccessibility mDreamAccessibility;
private boolean mShouldShowComplications;
private DreamServiceWrapper mDreamServiceWrapper;
@@ -455,6 +458,15 @@
/** {@inheritDoc} */
@Override
public void onWindowFocusChanged(boolean hasFocus) {
+ if (!dreamTracksFocus()) {
+ return;
+ }
+
+ try {
+ mDreamManager.onDreamFocusChanged(hasFocus);
+ } catch (RemoteException ex) {
+ // system server died
+ }
}
/** {@inheritDoc} */
@@ -664,6 +676,7 @@
*/
public void setInteractive(boolean interactive) {
mInteractive = interactive;
+ updateAccessibilityMessage();
}
/**
@@ -1149,6 +1162,19 @@
wakeUp(false);
}
+ /**
+ * Tells the dream to come to the front (which in turn tells the overlay to come to the front).
+ */
+ private void comeToFront() {
+ mOverlayConnection.addConsumer(overlay -> {
+ try {
+ overlay.comeToFront();
+ } catch (RemoteException e) {
+ Log.e(mTag, "could not tell overlay to come to front:" + e);
+ }
+ });
+ }
+
private void wakeUp(boolean fromSystem) {
if (mDebug) {
Slog.v(mTag, "wakeUp(): fromSystem=" + fromSystem + ", mWaking=" + mWaking
@@ -1430,7 +1456,7 @@
// Hide all insets when the dream is showing
mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
mWindow.setDecorFitsSystemWindows(false);
-
+ updateAccessibilityMessage();
mWindow.getDecorView().addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
private Consumer<IDreamOverlayClient> mDreamStartOverlayConsumer;
@@ -1477,6 +1503,15 @@
});
}
+ private void updateAccessibilityMessage() {
+ if (mWindow == null) return;
+ if (mDreamAccessibility == null) {
+ final View rootView = mWindow.getDecorView();
+ mDreamAccessibility = new DreamAccessibility(this, rootView);
+ }
+ mDreamAccessibility.updateAccessibilityConfiguration(isInteractive());
+ }
+
private boolean getWindowFlagValue(int flag, boolean defaultValue) {
return mWindow == null ? defaultValue : (mWindow.getAttributes().flags & flag) != 0;
}
@@ -1596,6 +1631,15 @@
public void wakeUp() {
mHandler.post(() -> DreamService.this.wakeUp(true /*fromSystem*/));
}
+
+ @Override
+ public void comeToFront() {
+ if (!dreamTracksFocus()) {
+ return;
+ }
+
+ mHandler.post(DreamService.this::comeToFront);
+ }
}
/** @hide */
diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl
index e45384f..85f0368 100644
--- a/core/java/android/service/dreams/IDreamManager.aidl
+++ b/core/java/android/service/dreams/IDreamManager.aidl
@@ -48,4 +48,5 @@
void setSystemDreamComponent(in ComponentName componentName);
void registerDreamOverlayService(in ComponentName componentName);
void startDreamActivity(in Intent intent);
+ void onDreamFocusChanged(in boolean hasFocus);
}
diff --git a/core/java/android/service/dreams/IDreamOverlayClient.aidl b/core/java/android/service/dreams/IDreamOverlayClient.aidl
index 78b7280..5054d4d 100644
--- a/core/java/android/service/dreams/IDreamOverlayClient.aidl
+++ b/core/java/android/service/dreams/IDreamOverlayClient.aidl
@@ -42,4 +42,7 @@
/** Called when the dream has ended. */
void endDream();
+
+ /** Called when the dream is coming to the front. */
+ void comeToFront();
}
diff --git a/core/java/android/service/dreams/IDreamService.aidl b/core/java/android/service/dreams/IDreamService.aidl
index 8b5d875..2e2651b 100644
--- a/core/java/android/service/dreams/IDreamService.aidl
+++ b/core/java/android/service/dreams/IDreamService.aidl
@@ -25,4 +25,5 @@
void attach(IBinder windowToken, boolean canDoze, boolean isPreviewMode, IRemoteCallback started);
void detach();
void wakeUp();
+ void comeToFront();
}
diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig
index 88f1090..0a458bc 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -19,3 +19,10 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "dream_tracks_focus"
+ namespace: "communal"
+ description: "This flag enables the ability for dreams to track whether or not they have focus"
+ bug: "331798001"
+}
diff --git a/core/java/android/service/dreams/utils/DreamAccessibility.java b/core/java/android/service/dreams/utils/DreamAccessibility.java
new file mode 100644
index 0000000..c38f41b
--- /dev/null
+++ b/core/java/android/service/dreams/utils/DreamAccessibility.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.dreams.utils;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+/**
+ * {@link DreamAccessibility} allows customization of accessibility
+ * actions for the root view of the dream overlay.
+ * @hide
+ */
+public class DreamAccessibility {
+ private final Context mContext;
+ private final View mView;
+ private final View.AccessibilityDelegate mAccessibilityDelegate;
+
+ public DreamAccessibility(@NonNull Context context, @NonNull View view) {
+ mContext = context;
+ mView = view;
+ mAccessibilityDelegate = createNewAccessibilityDelegate(mContext);
+ }
+
+ /**
+ * @param interactive
+ * Removes and add accessibility configuration depending if the dream is interactive or not
+ */
+ public void updateAccessibilityConfiguration(Boolean interactive) {
+ if (!interactive) {
+ addAccessibilityConfiguration();
+ } else {
+ removeCustomAccessibilityAction();
+ }
+ }
+
+ /**
+ * Configures the accessibility actions for the given root view.
+ */
+ private void addAccessibilityConfiguration() {
+ mView.setAccessibilityDelegate(mAccessibilityDelegate);
+ }
+
+ /**
+ * Removes Configured the accessibility actions for the given root view.
+ */
+ private void removeCustomAccessibilityAction() {
+ if (mView.getAccessibilityDelegate() == mAccessibilityDelegate) {
+ mView.setAccessibilityDelegate(null);
+ }
+ }
+
+ private View.AccessibilityDelegate createNewAccessibilityDelegate(Context context) {
+ return new View.AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ for (AccessibilityNodeInfo.AccessibilityAction action : info.getActionList()) {
+ if (action.getId() == AccessibilityNodeInfo.ACTION_CLICK) {
+ info.removeAction(action);
+ break;
+ }
+ }
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK,
+ context.getResources().getString(R.string.dream_accessibility_action_click)
+ ));
+ }
+ };
+ }
+}
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index 786d768..1d7091c 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -349,23 +349,29 @@
/**
* Indicates no explicit setting for which channels may bypass DND when this policy is active.
* Defaults to {@link #CHANNEL_POLICY_PRIORITY}.
+ *
+ * @hide
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- private static final int CHANNEL_POLICY_UNSET = 0;
+ public static final int CHANNEL_POLICY_UNSET = 0;
/**
* Indicates that channels marked as {@link NotificationChannel#canBypassDnd()} can bypass DND
* when this policy is active.
+ *
+ * @hide
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- private static final int CHANNEL_POLICY_PRIORITY = 1;
+ public static final int CHANNEL_POLICY_PRIORITY = 1;
/**
* Indicates that no channels can bypass DND when this policy is active, even those marked as
* {@link NotificationChannel#canBypassDnd()}.
+ *
+ * @hide
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- private static final int CHANNEL_POLICY_NONE = 2;
+ public static final int CHANNEL_POLICY_NONE = 2;
/** @hide */
public ZenPolicy() {
@@ -564,6 +570,13 @@
}
/**
+ * @hide
+ */
+ public @ChannelType int getAllowedChannels() {
+ return mAllowChannels;
+ }
+
+ /**
* Whether this policy allows {@link NotificationChannel channels} marked as
* {@link NotificationChannel#canBypassDnd()} to bypass DND. If {@link #STATE_ALLOW}, these
* channels may bypass; if {@link #STATE_DISALLOW}, then even notifications from channels
@@ -999,6 +1012,12 @@
mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE;
return this;
}
+
+ /** @hide */
+ public @NonNull Builder allowChannels(@ChannelType int channelType) {
+ mZenPolicy.mAllowChannels = channelType;
+ return this;
+ }
}
@Override
diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java
index fb57921..4281da1 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -1685,6 +1685,10 @@
public final void onSimultaneousCallingStateChanged(int[] subIds) {
// not supported on the deprecated interface - Use TelephonyCallback instead
}
+
+ public final void onCarrierRoamingNtnModeChanged(boolean active) {
+ // not supported on the deprecated interface - Use TelephonyCallback instead
+ }
}
private void log(String s) {
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index dc6a035..b8b84d9 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -644,6 +644,15 @@
public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41;
/**
+ * Event for listening to changes in carrier roaming non-terrestrial network mode.
+ *
+ * @see CarrierRoamingNtnModeListener
+ *
+ * @hide
+ */
+ public static final int EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED = 42;
+
+ /**
* @hide
*/
@IntDef(prefix = {"EVENT_"}, value = {
@@ -687,7 +696,8 @@
EVENT_TRIGGER_NOTIFY_ANBR,
EVENT_MEDIA_QUALITY_STATUS_CHANGED,
EVENT_EMERGENCY_CALLBACK_MODE_CHANGED,
- EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED
+ EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED,
+ EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED
})
@Retention(RetentionPolicy.SOURCE)
public @interface TelephonyEvent {
@@ -1686,6 +1696,24 @@
}
/**
+ * Interface for carrier roaming non-terrestrial network listener.
+ *
+ * @hide
+ */
+ public interface CarrierRoamingNtnModeListener {
+ /**
+ * Callback invoked when carrier roaming non-terrestrial network mode changes.
+ *
+ * @param active {@code true} If the device is connected to carrier roaming
+ * non-terrestrial network or was connected within the
+ * {CarrierConfigManager
+ * #KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT} duration,
+ * {code false} otherwise.
+ */
+ void onCarrierRoamingNtnModeChanged(boolean active);
+ }
+
+ /**
* The callback methods need to be called on the handler thread where
* this object was created. If the binder did that for us it'd be nice.
* <p>
@@ -2086,5 +2114,16 @@
Binder.withCleanCallingIdentity(
() -> mExecutor.execute(() -> listener.onCallBackModeStopped(type, reason)));
}
+
+ public void onCarrierRoamingNtnModeChanged(boolean active) {
+ if (!Flags.carrierEnabledSatelliteFlag()) return;
+
+ CarrierRoamingNtnModeListener listener =
+ (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get();
+ if (listener == null) return;
+
+ Binder.withCleanCallingIdentity(
+ () -> mExecutor.execute(() -> listener.onCarrierRoamingNtnModeChanged(active)));
+ }
}
}
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index d39c4ce..6160fdb 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -1073,6 +1073,24 @@
}
/**
+ * Notify external listeners that carrier roaming non-terrestrial network mode changed.
+ * @param subId subscription ID.
+ * @param active {@code true} If the device is connected to carrier roaming
+ * non-terrestrial network or was connected within the
+ * {CarrierConfigManager#KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT}
+ * duration, {code false} otherwise.
+ * @hide
+ */
+ public void notifyCarrierRoamingNtnModeChanged(int subId, boolean active) {
+ try {
+ sRegistry.notifyCarrierRoamingNtnModeChanged(subId, active);
+ } catch (RemoteException ex) {
+ // system server crash
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Processes potential event changes from the provided {@link TelephonyCallback}.
*
* @param telephonyCallback callback for monitoring callback changes to the telephony state.
@@ -1224,6 +1242,10 @@
eventList.add(
TelephonyCallback.EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED);
}
+
+ if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnModeListener) {
+ eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED);
+ }
return eventList;
}
diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java
index 38aeb37..161a79b 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -30,6 +30,8 @@
import android.graphics.Rect;
import android.graphics.text.LineBreakConfig;
import android.graphics.text.MeasuredText;
+import android.icu.lang.UCharacter;
+import android.icu.lang.UCharacterDirection;
import android.icu.text.Bidi;
import android.text.AutoGrowArray.ByteArray;
import android.text.AutoGrowArray.FloatArray;
@@ -711,7 +713,12 @@
// check the paragraph count here and replace the CR letters and re-calculate
// BiDi again.
for (int i = 0; i < mTextLength; ++i) {
- if (mCopiedBuffer[i] == '\r') {
+ if (Character.isSurrogate(mCopiedBuffer[i])) {
+ // All block separators are in BMP.
+ continue;
+ }
+ if (UCharacter.getDirection(mCopiedBuffer[i])
+ == UCharacterDirection.BLOCK_SEPARATOR) {
mCopiedBuffer[i] = OBJECT_REPLACEMENT_CHARACTER;
}
}
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index bde9c77..3015791 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -477,7 +477,12 @@
}
drawBounds.setEmpty();
float w = measure(mLen, false, fmi, drawBounds, lineInfo);
- float boundsWidth = drawBounds.width();
+ float boundsWidth;
+ if (w >= 0) {
+ boundsWidth = Math.max(drawBounds.right, w) - Math.min(0, drawBounds.left);
+ } else {
+ boundsWidth = Math.max(drawBounds.right, 0) - Math.min(w, drawBounds.left);
+ }
if (Math.abs(w) > boundsWidth) {
return w;
} else {
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index 51d7caa..65d9b3a 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -291,6 +291,15 @@
int getDefaultDisplayRotation();
/**
+ * Retrieve the display user rotation.
+ * @param displayId Id of the display
+ * @return Rotation one of {@link android.view.Surface#ROTATION_0},
+ * {@link android.view.Surface#ROTATION_90}, {@link android.view.Surface#ROTATION_180},
+ * {@link android.view.Surface#ROTATION_270} or -1 if display is not found.
+ */
+ int getDisplayUserRotation(int displayId);
+
+ /**
* Watch the rotation of the specified screen. Returns the current rotation,
* calls back when it changes.
*/
diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java
index 6c852c3..86e5bea 100644
--- a/core/java/android/view/ViewStructure.java
+++ b/core/java/android/view/ViewStructure.java
@@ -76,6 +76,21 @@
"android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION";
/**
+ * Key used for writing the type of the view that generated the virtual structure of its
+ * children.
+ *
+ * For example, if the virtual structure is generated by a webview, the value would be
+ * "WebView". If the virtual structure is generated by a compose view, then the value would be
+ * "ComposeView". The value is of type String.
+ *
+ * This value is added to mainly help with debugging purpose.
+ *
+ * @hide
+ */
+ public static final String EXTRA_VIRTUAL_STRUCTURE_TYPE =
+ "android.view.ViewStructure.extra.VIRTUAL_STRUCTURE_TYPE";
+
+ /**
* Set the identifier for this view.
*
* @param id The view's identifier, as per {@link View#getId View.getId()}.
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index fbb5116..fb1c331 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -11254,8 +11254,10 @@
width = des;
} else {
if (mUseBoundsForWidth) {
- width = Math.max(boring.width,
- (int) Math.ceil(boring.getDrawingBoundingBox().width()));
+ RectF bbox = boring.getDrawingBoundingBox();
+ float rightMax = Math.max(bbox.right, boring.width);
+ float leftMin = Math.min(bbox.left, 0);
+ width = Math.max(boring.width, (int) Math.ceil(rightMax - leftMin));
} else {
width = boring.width;
}
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index 4c2bbd4..c8d6810 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -1,4 +1,5 @@
package: "com.android.internal.os"
+container: "system"
flag {
name: "enable_apache_http_legacy_preload"
@@ -7,4 +8,4 @@
# Fixed read-only is required as the flag is read during zygote init.
is_fixed_read_only: true
bug: "241474956"
-}
\ No newline at end of file
+}
diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
index 969f95d..792c223 100644
--- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
+++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
@@ -81,4 +81,5 @@
void onCallBackModeStarted(int type);
void onCallBackModeStopped(int type, int reason);
void onSimultaneousCallingStateChanged(in int[] subIds);
+ void onCarrierRoamingNtnModeChanged(in boolean active);
}
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index 0203ea4..04332cd 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -120,4 +120,5 @@
void notifyCallbackModeStarted(int phoneId, int subId, int type);
void notifyCallbackModeStopped(int phoneId, int subId, int type, int reason);
+ void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active);
}
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 60289c1..e96240d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1018,6 +1018,9 @@
<!-- The title to use when a dream is opened in preview mode. [CHAR LIMIT=NONE] -->
<string name="dream_preview_title">Preview, <xliff:g id="dream_name" example="Clock">%1$s</xliff:g></string>
+ <!-- The title to use when a dream is open in accessibility mode to let users know to double tap to dismiss the dream [CHAR LIMIT=32] -->
+ <string name="dream_accessibility_action_click">dismiss</string>
+
<!-- Permissions -->
<!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 624026c..0bf6347 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4471,6 +4471,7 @@
<java-symbol type="string" name="capability_title_canTakeScreenshot" />
<java-symbol type="string" name="dream_preview_title" />
+ <java-symbol type="string" name="dream_accessibility_action_click" />
<java-symbol type="string" name="config_servicesExtensionPackage" />
diff --git a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
index c9cb2cc..e25fdf9 100644
--- a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
@@ -90,6 +90,12 @@
assertEquals(ddl, getSqlStatementType("ALTER TABLE t1 ADD COLUMN j int"));
assertEquals(ddl, getSqlStatementType("CREATE TABLE t1 (i int)"));
+ // Verify that the answers are case-insensitive
+ assertEquals(sel, getSqlStatementType("select"));
+ assertEquals(sel, getSqlStatementType("sElect"));
+ assertEquals(sel, getSqlStatementType("sELECT"));
+ assertEquals(sel, getSqlStatementType("seLECT"));
+
// Short statements, leading comments, and WITH are decoded to "other" in the public API.
final int othr = STATEMENT_OTHER;
assertEquals(othr, getSqlStatementType("SE"));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
index a67821b..0297901 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
@@ -288,13 +288,16 @@
/** Sets the position of the dot and badge, animating them out and back in if requested. */
void animateDotBadgePositions(boolean onLeft) {
- mOnLeft = onLeft;
-
- if (onLeft != getDotOnLeft() && shouldDrawDot()) {
- animateDotScale(0f /* showDot */, () -> {
- invalidate();
- animateDotScale(1.0f, null /* after */);
- });
+ if (onLeft != getDotOnLeft()) {
+ if (shouldDrawDot()) {
+ animateDotScale(0f /* showDot */, () -> {
+ mOnLeft = onLeft;
+ invalidate();
+ animateDotScale(1.0f, null /* after */);
+ });
+ } else {
+ mOnLeft = onLeft;
+ }
}
// TODO animate badge
showBadge();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 74f087b..0b3c2ba3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -446,6 +446,8 @@
mManageButton.setVisibility(GONE);
} else {
mTaskView = bubbleTaskView.getTaskView();
+ // reset the insets that might left after TaskView is shown in BubbleBarExpandedView
+ mTaskView.setCaptionInsets(null);
bubbleTaskView.setDelegateListener(mTaskViewListener);
// set a fixed width so it is not recalculated as part of a rotation. the width will be
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 57cf992..e885262 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -597,6 +597,17 @@
return;
}
+ if (mPipTransitionState.isEnteringPip()
+ && !mPipTransitionState.getInSwipePipToHomeTransition()) {
+ // If we are still entering PiP with Shell playing enter animation, jump-cut to
+ // the end of the enter animation and reschedule exitPip to run after enter-PiP
+ // has finished its transition and allowed the client to draw in PiP mode.
+ mPipTransitionController.end(() -> {
+ exitPip(animationDurationMs, requestEnterSplit);
+ });
+ return;
+ }
+
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
final WindowContainerTransaction wct = new WindowContainerTransaction();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 10b7619..fdde3ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -43,7 +43,6 @@
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
-import android.animation.Animator;
import android.annotation.IntDef;
import android.app.ActivityManager;
import android.app.TaskInfo;
@@ -348,9 +347,16 @@
@Override
public void end() {
- Animator animator = mPipAnimationController.getCurrentAnimator();
- if (animator != null && animator.isRunning()) {
- animator.end();
+ end(null);
+ }
+
+ @Override
+ public void end(@Nullable Runnable onTransitionEnd) {
+ if (mPipAnimationController.isAnimating()) {
+ mPipAnimationController.getCurrentAnimator().end();
+ }
+ if (onTransitionEnd != null) {
+ onTransitionEnd.run();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index 32442f7..4f71a02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -305,6 +305,14 @@
public void end() {
}
+ /**
+ * End the currently-playing PiP animation.
+ *
+ * @param onTransitionEnd callback to run upon finishing the playing transition.
+ */
+ public void end(@Nullable Runnable onTransitionEnd) {
+ }
+
/** Starts the {@link android.window.SystemPerformanceHinter.HighPerfSession}. */
public void startHighPerfSession() {}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt
new file mode 100644
index 0000000..5563bb9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class CloseAllAppWithAppHeaderExitLandscape : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_90) {
+ @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"])
+ @Test
+ override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig()
+ .use(FlickerServiceConfig.DEFAULT)
+ .use(CLOSE_APP)
+ .use(CLOSE_LAST_APP)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt
new file mode 100644
index 0000000..3d16d2219
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class CloseAllAppWithAppHeaderExitPortrait : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_0) {
+ @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"])
+ @Test
+ override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig()
+ .use(FlickerServiceConfig.DEFAULT)
+ .use(CLOSE_APP)
+ .use(CLOSE_LAST_APP)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt
new file mode 100644
index 0000000..75dfeba
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.flicker
+
+import android.tools.flicker.AssertionInvocationGroup
+import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd
+import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
+import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart
+import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
+import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd
+import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart
+import android.tools.flicker.assertors.assertions.LauncherWindowMovesToTop
+import android.tools.flicker.config.AssertionTemplates
+import android.tools.flicker.config.FlickerConfigEntry
+import android.tools.flicker.config.ScenarioId
+import android.tools.flicker.config.desktopmode.Components
+import android.tools.flicker.extractors.ITransitionMatcher
+import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
+import android.tools.traces.wm.Transition
+import android.tools.traces.wm.TransitionType
+
+class DesktopModeFlickerScenarios {
+ companion object {
+ val END_DRAG_TO_DESKTOP =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"),
+ extractor =
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ return transitions.filter {
+ it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP
+ }
+ }
+ }
+ ),
+ assertions =
+ AssertionTemplates.COMMON_ASSERTIONS +
+ listOf(
+ AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP),
+ AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
+ AppWindowHasDesktopModeInitialBoundsAtTheEnd(
+ Components.DESKTOP_MODE_APP
+ )
+ )
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
+ val CLOSE_APP =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("CLOSE_APP"),
+ extractor =
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ return transitions.filter { it.type == TransitionType.CLOSE }
+ }
+ }
+ ),
+ assertions =
+ AssertionTemplates.COMMON_ASSERTIONS +
+ listOf(
+ AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP),
+ AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP),
+ AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP),
+ )
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
+ val CLOSE_LAST_APP =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("CLOSE_LAST_APP"),
+ extractor =
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ val lastTransition =
+ transitions.findLast { it.type == TransitionType.CLOSE }
+ return if (lastTransition != null) listOf(lastTransition)
+ else emptyList()
+ }
+ }
+ ),
+ assertions =
+ AssertionTemplates.COMMON_ASSERTIONS +
+ listOf(
+ AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP),
+ AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP),
+ AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP),
+ LauncherWindowMovesToTop()
+ )
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt
index 4c781d3..9dfafe9 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt
@@ -17,58 +17,28 @@
package com.android.wm.shell.flicker.service.desktopmode.flicker
import android.tools.Rotation
-import android.tools.flicker.AssertionInvocationGroup
import android.tools.flicker.FlickerConfig
import android.tools.flicker.annotation.ExpectedScenarios
import android.tools.flicker.annotation.FlickerConfigProvider
-import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
-import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
-import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd
-import android.tools.flicker.config.AssertionTemplates
import android.tools.flicker.config.FlickerConfig
-import android.tools.flicker.config.FlickerConfigEntry
import android.tools.flicker.config.FlickerServiceConfig
-import android.tools.flicker.config.ScenarioId
-import android.tools.flicker.config.desktopmode.Components
-import android.tools.flicker.extractors.ITransitionMatcher
-import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
-import android.tools.traces.wm.Transition
-import android.tools.traces.wm.TransitionType
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP
import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class EnterDesktopWithDragLandscape : EnterDesktopWithDrag(Rotation.ROTATION_90) {
- @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() =
- super.enterDesktopWithDrag()
+ @ExpectedScenarios(["END_DRAG_TO_DESKTOP"])
+ @Test
+ override fun enterDesktopWithDrag() = super.enterDesktopWithDrag()
companion object {
- private val END_DRAG_TO_DESKTOP = FlickerConfigEntry(
- scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"),
- extractor = ShellTransitionScenarioExtractor(
- transitionMatcher = object : ITransitionMatcher {
- override fun findAll(
- transitions: Collection<Transition>
- ): Collection<Transition> {
- return transitions.filter {
- it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP}
- }
- }),
- assertions = AssertionTemplates.COMMON_ASSERTIONS +
- listOf(
- AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP),
- AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
- AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP)
- ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
- )
@JvmStatic
@FlickerConfigProvider
fun flickerConfigProvider(): FlickerConfig =
- FlickerConfig()
- .use(FlickerServiceConfig.DEFAULT)
- .use(END_DRAG_TO_DESKTOP)
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt
index d99d875..1c7d623 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt
@@ -17,58 +17,27 @@
package com.android.wm.shell.flicker.service.desktopmode.flicker
import android.tools.Rotation
-import android.tools.flicker.AssertionInvocationGroup
import android.tools.flicker.FlickerConfig
import android.tools.flicker.annotation.ExpectedScenarios
import android.tools.flicker.annotation.FlickerConfigProvider
-import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
-import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
-import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd
-import android.tools.flicker.config.AssertionTemplates
import android.tools.flicker.config.FlickerConfig
-import android.tools.flicker.config.FlickerConfigEntry
import android.tools.flicker.config.FlickerServiceConfig
-import android.tools.flicker.config.ScenarioId
-import android.tools.flicker.config.desktopmode.Components
-import android.tools.flicker.extractors.ITransitionMatcher
-import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
-import android.tools.traces.wm.Transition
-import android.tools.traces.wm.TransitionType
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP
import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(FlickerServiceJUnit4ClassRunner::class)
class EnterDesktopWithDragPortrait : EnterDesktopWithDrag(Rotation.ROTATION_0) {
- @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() =
- super.enterDesktopWithDrag()
+ @ExpectedScenarios(["END_DRAG_TO_DESKTOP"])
+ @Test
+ override fun enterDesktopWithDrag() = super.enterDesktopWithDrag()
companion object {
- private val END_DRAG_TO_DESKTOP = FlickerConfigEntry(
- scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"),
- extractor = ShellTransitionScenarioExtractor(
- transitionMatcher = object : ITransitionMatcher {
- override fun findAll(
- transitions: Collection<Transition>
- ): Collection<Transition> {
- return transitions.filter {
- it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP}
- }
- }),
- assertions = AssertionTemplates.COMMON_ASSERTIONS +
- listOf(
- AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP),
- AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
- AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP)
- ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
- )
-
@JvmStatic
@FlickerConfigProvider
fun flickerConfigProvider(): FlickerConfig =
- FlickerConfig()
- .use(FlickerServiceConfig.DEFAULT)
- .use(END_DRAG_TO_DESKTOP)
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt
new file mode 100644
index 0000000..0c2b501
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.MailAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class CloseAllAppsWithAppHeaderExit
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation))
+ private val nonResizeableApp = DesktopModeAppHelper(NonResizeableAppHelper(instrumentation))
+
+
+
+ @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode())
+ tapl.setEnableRotation(true)
+ tapl.setExpectedRotation(rotation.value)
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ mailApp.launchViaIntent(wmHelper)
+ nonResizeableApp.launchViaIntent(wmHelper)
+ }
+
+ @Test
+ open fun closeAllAppsInDesktop() {
+ nonResizeableApp.closeDesktopApp(wmHelper, device)
+ mailApp.closeDesktopApp(wmHelper, device)
+ testApp.closeDesktopApp(wmHelper, device)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt
index 0403b4f..9e9998e 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt
@@ -23,15 +23,18 @@
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
import com.android.wm.shell.flicker.service.common.Utils
-import com.android.wm.shell.flicker.utils.DesktopModeUtils
import org.junit.After
+import org.junit.Assume
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
+
@Ignore("Base Test Class")
abstract class EnterDesktopWithDrag
@JvmOverloads
@@ -41,19 +44,20 @@
private val tapl = LauncherInstrumentation()
private val wmHelper = WindowManagerStateHelper(instrumentation)
private val device = UiDevice.getInstance(instrumentation)
- private val testApp = SimpleAppHelper(instrumentation)
+ private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
@Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
@Before
fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode())
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
}
@Test
open fun enterDesktopWithDrag() {
- DesktopModeUtils.enterDesktopWithDrag(wmHelper, device, testApp)
+ testApp.enterDesktopWithDrag(wmHelper, device)
}
@After
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt
deleted file mode 100644
index 345bc5e..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.flicker.utils
-
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.helpers.SYSTEMUI_PACKAGE
-import android.tools.traces.component.IComponentMatcher
-import android.tools.traces.parsers.WindowManagerStateHelper
-import android.tools.traces.wm.WindowingMode
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.Until
-
-/**
- * Provides a collection of utility functions for desktop mode testing.
- */
-object DesktopModeUtils {
- private const val TIMEOUT_MS = 3_000L
- private const val CAPTION = "desktop_mode_caption"
- private const val CAPTION_HANDLE = "caption_handle"
- private const val MAXIMIZE_BUTTON = "maximize_button_view"
-
- private val captionFullscreen: BySelector
- get() = By.res(SYSTEMUI_PACKAGE, CAPTION)
- private val captionHandle: BySelector
- get() = By.res(SYSTEMUI_PACKAGE, CAPTION_HANDLE)
- private val maximizeButton: BySelector
- get() = By.res(SYSTEMUI_PACKAGE, MAXIMIZE_BUTTON)
-
- /**
- * Wait for an app moved to desktop to finish its transition.
- */
- private fun waitForAppToMoveToDesktop(
- wmHelper: WindowManagerStateHelper,
- currentApp: IComponentMatcher,
- ) {
- wmHelper
- .StateSyncBuilder()
- .withWindowSurfaceAppeared(currentApp)
- .withFreeformApp(currentApp)
- .withAppTransitionIdle()
- .waitForAndVerify()
- }
-
- /**
- * Click maximise button on the app header for the given app.
- */
- fun maximiseDesktopApp(
- wmHelper: WindowManagerStateHelper,
- device: UiDevice,
- currentApp: StandardAppHelper
- ) {
- if (wmHelper.getWindow(currentApp)?.windowingMode
- != WindowingMode.WINDOWING_MODE_FREEFORM.value)
- error("expected a freeform window to maximise but window is not in freefrom mode")
-
- val maximizeButton =
- device.wait(Until.findObject(maximizeButton), TIMEOUT_MS)
- ?: error("Unable to find view $maximizeButton\n")
- maximizeButton.click()
- }
-
- /**
- * Move an app to Desktop by dragging the app handle at the top.
- */
- fun enterDesktopWithDrag(
- wmHelper: WindowManagerStateHelper,
- device: UiDevice,
- currentApp: StandardAppHelper,
- ) {
- currentApp.launchViaIntent(wmHelper)
- dragToDesktop(wmHelper, currentApp, device)
- waitForAppToMoveToDesktop(wmHelper, currentApp)
- }
-
- private fun dragToDesktop(
- wmHelper: WindowManagerStateHelper,
- currentApp: StandardAppHelper,
- device: UiDevice
- ) {
- val windowRect = wmHelper.getWindowRegion(currentApp).bounds
- val startX = windowRect.centerX()
-
- // Start dragging a little under the top to prevent dragging the notification shade.
- val startY = 10
-
- val displayRect =
- wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
- ?: throw IllegalStateException("Default display is null")
-
- // The position we want to drag to
- val endY = displayRect.centerY() / 2
-
- // drag the window to move to desktop
- device.drag(startX, startY, startX, endY, 100)
- }
-}
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index b870023..8e07a2f 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -15,14 +15,18 @@
*/
#include "ShaderCache.h"
+
#include <GrDirectContext.h>
#include <SkData.h>
#include <gui/TraceUtils.h>
#include <log/log.h>
#include <openssl/sha.h>
+
#include <algorithm>
#include <array>
+#include <mutex>
#include <thread>
+
#include "FileBlobCache.h"
#include "Properties.h"
diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp
index 6993d52..7a155c5 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.cpp
+++ b/libs/hwui/renderthread/HintSessionWrapper.cpp
@@ -45,7 +45,7 @@
LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!");
BIND_APH_METHOD(getManager);
- BIND_APH_METHOD(createSession);
+ BIND_APH_METHOD(createSessionInternal);
BIND_APH_METHOD(closeSession);
BIND_APH_METHOD(updateTargetWorkDuration);
BIND_APH_METHOD(reportActualWorkDuration);
@@ -122,7 +122,8 @@
int64_t targetDurationNanos =
mLastTargetWorkDuration == 0 ? kDefaultTargetDuration : mLastTargetWorkDuration;
mHintSessionFuture = CommonPool::async([=, this, tids = mPermanentSessionTids] {
- return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos);
+ return mBinding->createSessionInternal(manager, tids.data(), tids.size(),
+ targetDurationNanos, SessionTag::HWUI);
});
return false;
}
diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h
index 14e7a53..859cc57 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.h
+++ b/libs/hwui/renderthread/HintSessionWrapper.h
@@ -17,6 +17,7 @@
#pragma once
#include <android/performance_hint.h>
+#include <private/performance_hint_private.h>
#include <future>
#include <optional>
@@ -80,9 +81,10 @@
virtual ~HintSessionBinding() = default;
virtual void init();
APerformanceHintManager* (*getManager)();
- APerformanceHintSession* (*createSession)(APerformanceHintManager* manager,
- const int32_t* tids, size_t tidCount,
- int64_t defaultTarget) = nullptr;
+ APerformanceHintSession* (*createSessionInternal)(APerformanceHintManager* manager,
+ const int32_t* tids, size_t tidCount,
+ int64_t defaultTarget,
+ SessionTag tag) = nullptr;
void (*closeSession)(APerformanceHintSession* session) = nullptr;
void (*updateTargetWorkDuration)(APerformanceHintSession* session,
int64_t targetDuration) = nullptr;
diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
index c16602c..a8db0f4 100644
--- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
+++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
@@ -52,8 +52,8 @@
void init() override;
MOCK_METHOD(APerformanceHintManager*, fakeGetManager, ());
- MOCK_METHOD(APerformanceHintSession*, fakeCreateSession,
- (APerformanceHintManager*, const int32_t*, size_t, int64_t));
+ MOCK_METHOD(APerformanceHintSession*, fakeCreateSessionInternal,
+ (APerformanceHintManager*, const int32_t*, size_t, int64_t, SessionTag));
MOCK_METHOD(void, fakeCloseSession, (APerformanceHintSession*));
MOCK_METHOD(void, fakeUpdateTargetWorkDuration, (APerformanceHintSession*, int64_t));
MOCK_METHOD(void, fakeReportActualWorkDuration, (APerformanceHintSession*, int64_t));
@@ -72,22 +72,28 @@
// Must be static so we can point to them as normal fn pointers with HintSessionBinding
static APerformanceHintManager* stubGetManager() { return sMockBinding->fakeGetManager(); };
- static APerformanceHintSession* stubCreateSession(APerformanceHintManager* manager,
- const int32_t* ids, size_t idsSize,
- int64_t initialTarget) {
- return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+ static APerformanceHintSession* stubCreateSessionInternal(APerformanceHintManager* manager,
+ const int32_t* ids, size_t idsSize,
+ int64_t initialTarget,
+ SessionTag tag) {
+ return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+ SessionTag::HWUI);
}
- static APerformanceHintSession* stubManagedCreateSession(APerformanceHintManager* manager,
- const int32_t* ids, size_t idsSize,
- int64_t initialTarget) {
+ static APerformanceHintSession* stubManagedCreateSessionInternal(
+ APerformanceHintManager* manager, const int32_t* ids, size_t idsSize,
+ int64_t initialTarget, SessionTag tag) {
sMockBinding->allowCreationToFinish.get_future().wait();
- return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+ return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+ SessionTag::HWUI);
}
- static APerformanceHintSession* stubSlowCreateSession(APerformanceHintManager* manager,
- const int32_t* ids, size_t idsSize,
- int64_t initialTarget) {
+ static APerformanceHintSession* stubSlowCreateSessionInternal(APerformanceHintManager* manager,
+ const int32_t* ids,
+ size_t idsSize,
+ int64_t initialTarget,
+ SessionTag tag) {
std::this_thread::sleep_for(50ms);
- return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+ return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+ SessionTag::HWUI);
}
static void stubCloseSession(APerformanceHintSession* session) {
sMockBinding->fakeCloseSession(session);
@@ -139,14 +145,14 @@
mWrapper = std::make_shared<HintSessionWrapper>(uiThreadId, renderThreadId);
mWrapper->mBinding = sMockBinding;
EXPECT_CALL(*sMockBinding, fakeGetManager).WillOnce(Return(managerPtr));
- ON_CALL(*sMockBinding, fakeCreateSession).WillByDefault(Return(sessionPtr));
+ ON_CALL(*sMockBinding, fakeCreateSessionInternal).WillByDefault(Return(sessionPtr));
ON_CALL(*sMockBinding, fakeSetThreads).WillByDefault(Return(0));
}
void HintSessionWrapperTests::MockHintSessionBinding::init() {
sMockBinding->getManager = &stubGetManager;
- if (sMockBinding->createSession == nullptr) {
- sMockBinding->createSession = &stubCreateSession;
+ if (sMockBinding->createSessionInternal == nullptr) {
+ sMockBinding->createSessionInternal = &stubCreateSessionInternal;
}
sMockBinding->closeSession = &stubCloseSession;
sMockBinding->updateTargetWorkDuration = &stubUpdateTargetWorkDuration;
@@ -163,14 +169,14 @@
TEST_F(HintSessionWrapperTests, destructorClosesBackgroundSession) {
EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
- sMockBinding->createSession = stubSlowCreateSession;
+ sMockBinding->createSessionInternal = stubSlowCreateSessionInternal;
mWrapper->init();
mWrapper = nullptr;
Mock::VerifyAndClearExpectations(sMockBinding.get());
}
TEST_F(HintSessionWrapperTests, sessionInitializesCorrectly) {
- EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1);
+ EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1);
mWrapper->init();
waitForWrapperReady();
}
@@ -219,7 +225,7 @@
// Here we test whether queueing delayedDestroy works while creation is still happening, if
// creation happens after
EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
- sMockBinding->createSession = &stubManagedCreateSession;
+ sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal;
// Start creating the session and destroying it at the same time
mWrapper->init();
@@ -246,7 +252,7 @@
// Here we test whether queueing delayedDestroy works while creation is still happening, if
// creation happens before
EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
- sMockBinding->createSession = &stubManagedCreateSession;
+ sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal;
// Start creating the session and destroying it at the same time
mWrapper->init();
@@ -352,7 +358,7 @@
}
TEST_F(HintSessionWrapperTests, setThreadsUpdatesSessionThreads) {
- EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1);
+ EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1);
EXPECT_CALL(*sMockBinding, fakeSetThreads(sessionPtr, testing::IsSupersetOf({11, 22})))
.Times(1);
mWrapper->init();
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 8a13c03..4492c85 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -2088,28 +2088,24 @@
}
return BAD_VALUE;
}
- size_t offset = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoOffset));
- size_t size = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoSize));
+ ssize_t offset = static_cast<ssize_t>(env->GetIntField(param, gFields.bufferInfoOffset));
+ ssize_t size = static_cast<ssize_t>(env->GetIntField(param, gFields.bufferInfoSize));
uint32_t flags = static_cast<uint32_t>(env->GetIntField(param, gFields.bufferInfoFlags));
- if (flags == 0 && size == 0) {
+ if (i == 0) {
+ *initialOffset = offset;
+ }
+ if (CC_UNLIKELY((offset < 0)
+ || (size < 0)
+ || ((INT32_MAX - offset) < size)
+ || ((offset - (*initialOffset)) != *totalSize))) {
if (errorDetailMsg) {
- *errorDetailMsg = "Error: Queuing an empty BufferInfo";
+ *errorDetailMsg = "Error: offset/size in BufferInfo";
}
return BAD_VALUE;
}
- if (i == 0) {
- *initialOffset = offset;
- if (CC_UNLIKELY(*initialOffset < 0)) {
- if (errorDetailMsg) {
- *errorDetailMsg = "Error: offset/size in BufferInfo";
- }
- return BAD_VALUE;
- }
- }
- if (CC_UNLIKELY(((ssize_t)(UINT32_MAX - offset) < (ssize_t)size)
- || ((offset - *initialOffset) != *totalSize))) {
+ if (flags == 0 && size == 0) {
if (errorDetailMsg) {
- *errorDetailMsg = "Error: offset/size in BufferInfo";
+ *errorDetailMsg = "Error: Queuing an empty BufferInfo";
}
return BAD_VALUE;
}
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 1c203e3..346c87d 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -366,6 +366,7 @@
APerformanceHint_setIHintManagerForTesting;
APerformanceHint_sendHint;
APerformanceHint_getThreadIds;
+ APerformanceHint_createSessionInternal;
extern "C++" {
ASurfaceControl_registerSurfaceStatsListener*;
ASurfaceControl_unregisterSurfaceStatsListener*;
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index fbb35e2..83056b2 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -463,6 +463,15 @@
return manager->createSession(threadIds, size, initialTargetWorkDurationNanos);
}
+APerformanceHintSession* APerformanceHint_createSessionInternal(
+ APerformanceHintManager* manager, const int32_t* threadIds, size_t size,
+ int64_t initialTargetWorkDurationNanos, SessionTag tag) {
+ VALIDATE_PTR(manager)
+ VALIDATE_PTR(threadIds)
+ return manager->createSession(threadIds, size, initialTargetWorkDurationNanos,
+ static_cast<hal::SessionTag>(tag));
+}
+
int64_t APerformanceHint_getPreferredUpdateRateNanos(APerformanceHintManager* manager) {
VALIDATE_PTR(manager)
return manager->getPreferredRateNanos();
@@ -486,9 +495,9 @@
delete session;
}
-int APerformanceHint_sendHint(void* session, SessionHint hint) {
+int APerformanceHint_sendHint(APerformanceHintSession* session, SessionHint hint) {
VALIDATE_PTR(session)
- return reinterpret_cast<APerformanceHintSession*>(session)->sendHint(hint);
+ return session->sendHint(hint);
}
int APerformanceHint_setThreads(APerformanceHintSession* session, const pid_t* threadIds,
@@ -498,11 +507,10 @@
return session->setThreads(threadIds, size);
}
-int APerformanceHint_getThreadIds(void* aPerformanceHintSession, int32_t* const threadIds,
+int APerformanceHint_getThreadIds(APerformanceHintSession* session, int32_t* const threadIds,
size_t* const size) {
- VALIDATE_PTR(aPerformanceHintSession)
- return static_cast<APerformanceHintSession*>(aPerformanceHintSession)
- ->getThreadIds(threadIds, size);
+ VALIDATE_PTR(session)
+ return session->getThreadIds(threadIds, size);
}
int APerformanceHint_setPreferPowerEfficiency(APerformanceHintSession* session, bool enabled) {
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index 974e6e6..58f56b8 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -30,9 +30,7 @@
#include <memory>
#include <vector>
-using aidl::android::hardware::power::SessionConfig;
-using aidl::android::hardware::power::SessionTag;
-using aidl::android::hardware::power::WorkDuration;
+namespace hal = aidl::android::hardware::power;
using aidl::android::os::IHintManager;
using aidl::android::os::IHintSession;
using ndk::ScopedAStatus;
@@ -45,7 +43,7 @@
public:
MOCK_METHOD(ScopedAStatus, createHintSessionWithConfig,
(const SpAIBinder& token, const ::std::vector<int32_t>& tids, int64_t durationNanos,
- SessionTag tag, std::optional<SessionConfig>* config,
+ hal::SessionTag tag, std::optional<hal::SessionConfig>* config,
std::shared_ptr<IHintSession>* _aidl_return),
(override));
MOCK_METHOD(ScopedAStatus, getHintSessionPreferredRate, (int64_t * _aidl_return), (override));
@@ -71,7 +69,7 @@
MOCK_METHOD(ScopedAStatus, setMode, (int32_t mode, bool enabled), (override));
MOCK_METHOD(ScopedAStatus, close, (), (override));
MOCK_METHOD(ScopedAStatus, reportActualWorkDuration2,
- (const ::std::vector<WorkDuration>& workDurations), (override));
+ (const ::std::vector<hal::WorkDuration>& workDurations), (override));
MOCK_METHOD(SpAIBinder, asBinder, (), (override));
MOCK_METHOD(bool, isRemote, (), (override));
};
@@ -95,7 +93,7 @@
return APerformanceHint_getManager();
}
APerformanceHintSession* createSession(APerformanceHintManager* manager,
- int64_t targetDuration = 56789L) {
+ int64_t targetDuration = 56789L, bool isHwui = false) {
mMockSession = ndk::SharedRefBase::make<NiceMock<MockIHintSession>>();
int64_t sessionId = 123;
std::vector<int32_t> tids;
@@ -104,8 +102,8 @@
ON_CALL(*mMockIHintManager,
createHintSessionWithConfig(_, Eq(tids), Eq(targetDuration), _, _, _))
- .WillByDefault(DoAll(SetArgPointee<4>(
- std::make_optional<SessionConfig>({.id = sessionId})),
+ .WillByDefault(DoAll(SetArgPointee<4>(std::make_optional<hal::SessionConfig>(
+ {.id = sessionId})),
SetArgPointee<5>(std::shared_ptr<IHintSession>(mMockSession)),
[] { return ScopedAStatus::ok(); }));
@@ -124,6 +122,10 @@
ON_CALL(*mMockSession, reportActualWorkDuration2(_)).WillByDefault([] {
return ScopedAStatus::ok();
});
+ if (isHwui) {
+ return APerformanceHint_createSessionInternal(manager, tids.data(), tids.size(),
+ targetDuration, SessionTag::HWUI);
+ }
return APerformanceHint_createSession(manager, tids.data(), tids.size(), targetDuration);
}
@@ -131,7 +133,7 @@
std::shared_ptr<NiceMock<MockIHintSession>> mMockSession = nullptr;
};
-bool equalsWithoutTimestamp(WorkDuration lhs, WorkDuration rhs) {
+bool equalsWithoutTimestamp(hal::WorkDuration lhs, hal::WorkDuration rhs) {
return lhs.workPeriodStartTimestampNanos == rhs.workPeriodStartTimestampNanos &&
lhs.cpuDurationNanos == rhs.cpuDurationNanos &&
lhs.gpuDurationNanos == rhs.gpuDurationNanos && lhs.durationNanos == rhs.durationNanos;
@@ -194,6 +196,16 @@
APerformanceHint_closeSession(session);
}
+TEST_F(PerformanceHintTest, TestHwuiSessionCreation) {
+ EXPECT_CALL(*mMockIHintManager,
+ createHintSessionWithConfig(_, _, _, hal::SessionTag::HWUI, _, _))
+ .Times(1);
+ APerformanceHintManager* manager = createManager();
+ APerformanceHintSession* session = createSession(manager, 56789L, true);
+ ASSERT_TRUE(session);
+ APerformanceHint_closeSession(session);
+}
+
TEST_F(PerformanceHintTest, SetThreads) {
APerformanceHintManager* manager = createManager();
@@ -249,8 +261,8 @@
return false;
}
for (int i = 0; i < expected.size(); ++i) {
- WorkDuration expectedWorkDuration = expected[i];
- WorkDuration actualWorkDuration = arg[i];
+ hal::WorkDuration expectedWorkDuration = expected[i];
+ hal::WorkDuration actualWorkDuration = arg[i];
if (!equalsWithoutTimestamp(expectedWorkDuration, actualWorkDuration)) {
*result_listener << "WorkDuration at [" << i << "] is different: "
<< "Expected: " << expectedWorkDuration.toString()
@@ -273,7 +285,7 @@
usleep(2); // Sleep for longer than preferredUpdateRateNanos.
struct TestPair {
- WorkDuration duration;
+ hal::WorkDuration duration;
int expectedResult;
};
std::vector<TestPair> testPairs{
@@ -282,7 +294,7 @@
{{1, -20, 1, 13, -8}, EINVAL},
};
for (auto&& pair : testPairs) {
- std::vector<WorkDuration> actualWorkDurations;
+ std::vector<hal::WorkDuration> actualWorkDurations;
actualWorkDurations.push_back(pair.duration);
EXPECT_CALL(*mMockSession, reportActualWorkDuration2(WorkDurationEq(actualWorkDurations)))
diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig
index cddbb6b..8cdef38 100644
--- a/packages/CrashRecovery/aconfig/flags.aconfig
+++ b/packages/CrashRecovery/aconfig/flags.aconfig
@@ -16,3 +16,11 @@
description: "Enables various dependencies of crashrecovery module"
bug: "289203818"
}
+
+flag {
+ name: "allow_rescue_party_flag_resets"
+ namespace: "crashrecovery"
+ description: "Enables rescue party flag resets"
+ bug: "287618292"
+ is_fixed_read_only: true
+}
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index bf69d3b..4437475 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
+ <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
<uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" />
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml
index 77979f0..8755247 100644
--- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml
+++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml
@@ -16,11 +16,12 @@
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:viewportWidth="18"
- android:viewportHeight="18"
- android:width="24dp"
- android:height="24dp">
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="@color/settingslib_materialColorOnPrimaryContainer">
<path
- android:pathData="M7 10l5 5 5 -5z"
- android:fillColor="@color/settingslib_materialColorOnPrimaryContainer"/>
+ android:fillColor="@android:color/white"
+ android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z"/>
</vector>
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt
index 8f67354..7ea4b18 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt
@@ -27,7 +27,7 @@
import kotlinx.coroutines.flow.flowOn
/** Content change flow for the given [uri]. */
-internal fun Context.contentChangeFlow(
+fun Context.contentChangeFlow(
uri: Uri,
sendInitial: Boolean = true,
): Flow<Unit> = callbackFlow {
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index a67839a..73c96d9 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -200,6 +200,10 @@
<string name="bluetooth_active_battery_level">Active. <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string>
<!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for untethered headset. [CHAR LIMIT=NONE] -->
<string name="bluetooth_active_battery_level_untethered">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g>, R: <xliff:g id="battery_level_as_percentage" example="25%">%2$s</xliff:g> battery.</string>
+ <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for the left part of the untethered headset. [CHAR LIMIT=NONE] -->
+ <string name="bluetooth_active_battery_level_untethered_left">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery</string>
+ <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for the right part of the untethered headset. [CHAR LIMIT=NONE] -->
+ <string name="bluetooth_active_battery_level_untethered_right">Active. R: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery</string>
<!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device battery level. [CHAR LIMIT=NONE] -->
<string name="bluetooth_battery_level"><xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery</string>
<!-- Connected devices settings. Message on TV when Bluetooth is connected but not in use, showing remote device battery level. [CHAR LIMIT=NONE] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 36a9ecf..a7b7da5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1476,30 +1476,13 @@
}
}
- // Try to show left/right information if can not get it from battery for hearing
+ // Try to show left/right information for hearing
// aids specifically.
boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid;
boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio
&& isConnectedHapClientDevice();
if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) {
- final Set<CachedBluetoothDevice> memberDevices = getMemberDevice();
- final CachedBluetoothDevice subDevice = getSubDevice();
- if (memberDevices.stream().anyMatch(m -> m.isConnected())) {
- stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
- } else if (subDevice != null && subDevice.isConnected()) {
- stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
- } else {
- int deviceSide = getDeviceSide();
- if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) {
- stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
- } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT) {
- stringRes = R.string.bluetooth_hearing_aid_left_active;
- } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_RIGHT) {
- stringRes = R.string.bluetooth_hearing_aid_right_active;
- } else {
- stringRes = R.string.bluetooth_active_no_battery_level;
- }
- }
+ return getHearingDeviceSummary(leftBattery, rightBattery, shortSummary);
}
}
}
@@ -1567,6 +1550,62 @@
return spannableBuilder;
}
+ private CharSequence getHearingDeviceSummary(int leftBattery, int rightBattery,
+ boolean shortSummary) {
+
+ CachedBluetoothDevice memberDevice = getMemberDevice().stream().filter(
+ CachedBluetoothDevice::isConnected).findFirst().orElse(null);
+ if (memberDevice == null && mSubDevice != null && mSubDevice.isConnected()) {
+ memberDevice = mSubDevice;
+ }
+
+ CachedBluetoothDevice leftDevice = null;
+ CachedBluetoothDevice rightDevice = null;
+ final int deviceSide = getDeviceSide();
+ if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT) {
+ leftDevice = this;
+ rightDevice = memberDevice;
+ } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_RIGHT) {
+ leftDevice = memberDevice;
+ rightDevice = this;
+ } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) {
+ leftDevice = this;
+ rightDevice = this;
+ }
+
+ if (leftBattery < 0 && leftDevice != null) {
+ leftBattery = leftDevice.getBatteryLevel();
+ }
+ if (rightBattery < 0 && rightDevice != null) {
+ rightBattery = rightDevice.getBatteryLevel();
+ }
+
+ if (leftDevice != null && rightDevice != null) {
+ if (leftBattery >= 0 && rightBattery >= 0 && !shortSummary) {
+ return mContext.getString(R.string.bluetooth_active_battery_level_untethered,
+ Utils.formatPercentage(leftBattery), Utils.formatPercentage(rightBattery));
+ } else {
+ return mContext.getString(R.string.bluetooth_hearing_aid_left_and_right_active);
+ }
+ } else if (leftDevice != null) {
+ if (leftBattery >= 0 && !shortSummary) {
+ return mContext.getString(R.string.bluetooth_active_battery_level_untethered_left,
+ Utils.formatPercentage(leftBattery));
+ } else {
+ return mContext.getString(R.string.bluetooth_hearing_aid_left_active);
+ }
+ } else if (rightDevice != null) {
+ if (rightBattery >= 0 && !shortSummary) {
+ return mContext.getString(R.string.bluetooth_active_battery_level_untethered_right,
+ Utils.formatPercentage(rightBattery));
+ } else {
+ return mContext.getString(R.string.bluetooth_hearing_aid_right_active);
+ }
+ }
+
+ return mContext.getString(R.string.bluetooth_active_no_battery_level);
+ }
+
private void addBatterySpan(SpannableStringBuilder builder,
String batteryString, boolean lowBattery, int lowBatteryColorRes) {
if (lowBattery && lowBatteryColorRes != SUMMARY_NO_COLOR_FOR_LOW_BATTERY) {
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 892d907..6174ab9 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -169,7 +169,6 @@
"androidx.compose.material_material-icons-extended",
"androidx.activity_activity-compose",
"androidx.compose.animation_animation-graphics",
- "androidx.test.rules",
],
libs: [
"keepanno-annotations",
@@ -426,6 +425,7 @@
kotlincflags: ["-Xjvm-default=all"],
optimize: {
shrink_resources: false,
+ optimized_shrink_resources: false,
proguard_flags_files: ["proguard.flags"],
},
@@ -515,6 +515,7 @@
optimize: true,
shrink: true,
shrink_resources: true,
+ optimized_shrink_resources: true,
ignore_warnings: false,
proguard_compatibility: false,
},
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 561c85e..5cc3caf 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1036,6 +1036,7 @@
<receiver
android:name=".statusbar.KeyboardShortcutsReceiver"
+ android:visibleToInstantApps="true"
android:exported="true">
<intent-filter>
<action android:name="com.android.intent.action.DISMISS_KEYBOARD_SHORTCUTS" />
@@ -1116,5 +1117,11 @@
android:name="android.service.dream"
android:resource="@xml/home_controls_dream_metadata" />
</service>
+
+ <activity android:name="com.android.systemui.keyboard.shortcut.ShortcutHelperActivity"
+ android:exported="false"
+ android:theme="@style/ShortcutHelperTheme"
+ android:excludeFromRecents="true"
+ android:finishOnCloseSystemDialogs="true" />
</application>
</manifest>
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index b499a0e..e38f347 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -776,6 +776,13 @@
}
flag {
+ name: "keyboard_shortcut_helper_rewrite"
+ namespace: "systemui"
+ description: "A new implementation of the keyboards shortcuts helper sheet."
+ bug: "327364197"
+}
+
+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/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 9ce30fd..1e60b98 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -599,6 +599,12 @@
)
}
+ init {
+ // We do this check here to cover all entry points, including Launcher which doesn't
+ // call startIntentWithAnimation()
+ if (!controller.isLaunching) TransitionAnimator.checkReturnAnimationFrameworkFlag()
+ }
+
@UiThread
internal fun postTimeouts() {
if (timeoutHandler != null) {
@@ -637,7 +643,28 @@
return
}
- startAnimation(apps, nonApps, callback)
+ val window = findRootTaskIfPossible(apps)
+ if (window == null) {
+ Log.i(TAG, "Aborting the animation as no window is opening")
+ callback?.invoke()
+
+ if (DEBUG_TRANSITION_ANIMATION) {
+ Log.d(
+ TAG,
+ "Calling controller.onTransitionAnimationCancelled() [no window opening]"
+ )
+ }
+ controller.onTransitionAnimationCancelled()
+ listener?.onTransitionAnimationCancelled()
+ return
+ }
+
+ val navigationBar =
+ nonApps?.firstOrNull {
+ it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
+ }
+
+ startAnimation(window, navigationBar, callback)
}
private fun findRootTaskIfPossible(
@@ -646,9 +673,17 @@
if (apps == null) {
return null
}
+
+ val targetMode =
+ if (controller.isLaunching) {
+ RemoteAnimationTarget.MODE_OPENING
+ } else {
+ RemoteAnimationTarget.MODE_CLOSING
+ }
var candidate: RemoteAnimationTarget? = null
+
for (it in apps) {
- if (it.mode == RemoteAnimationTarget.MODE_OPENING) {
+ if (it.mode == targetMode) {
if (activityTransitionUseLargestWindow()) {
if (
candidate == null ||
@@ -673,47 +708,31 @@
}
}
}
+
return candidate
}
private fun startAnimation(
- apps: Array<out RemoteAnimationTarget>?,
- nonApps: Array<out RemoteAnimationTarget>?,
+ window: RemoteAnimationTarget,
+ navigationBar: RemoteAnimationTarget?,
iCallback: IRemoteAnimationFinishedCallback?
) {
if (TransitionAnimator.DEBUG) {
Log.d(TAG, "Remote animation started")
}
- val window = findRootTaskIfPossible(apps)
- if (window == null) {
- Log.i(TAG, "Aborting the animation as no window is opening")
- iCallback?.invoke()
-
- if (DEBUG_TRANSITION_ANIMATION) {
- Log.d(
- TAG,
- "Calling controller.onTransitionAnimationCancelled() [no window opening]"
- )
- }
- controller.onTransitionAnimationCancelled()
- listener?.onTransitionAnimationCancelled()
- return
- }
-
- val navigationBar =
- nonApps?.firstOrNull {
- it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR
- }
-
val windowBounds = window.screenSpaceBounds
val endState =
- TransitionAnimator.State(
- top = windowBounds.top,
- bottom = windowBounds.bottom,
- left = windowBounds.left,
- right = windowBounds.right
- )
+ if (controller.isLaunching) {
+ TransitionAnimator.State(
+ top = windowBounds.top,
+ bottom = windowBounds.bottom,
+ left = windowBounds.left,
+ right = windowBounds.right
+ )
+ } else {
+ controller.createAnimatorState()
+ }
val windowBackgroundColor =
window.taskInfo?.let { callback.getBackgroundColor(it) } ?: window.backgroundColor
@@ -721,24 +740,29 @@
// instead of recomputing isExpandingFullyAbove here.
val isExpandingFullyAbove =
transitionAnimator.isExpandingFullyAbove(controller.transitionContainer, endState)
- val endRadius =
- if (isExpandingFullyAbove) {
- // Most of the time, expanding fully above the root view means expanding in full
- // screen.
- ScreenDecorationsUtils.getWindowCornerRadius(context)
- } else {
- // This usually means we are in split screen mode, so 2 out of 4 corners will
- // have
- // a radius of 0.
- 0f
- }
- endState.topCornerRadius = endRadius
- endState.bottomCornerRadius = endRadius
+ if (controller.isLaunching) {
+ val endRadius = getWindowRadius(isExpandingFullyAbove)
+ endState.topCornerRadius = endRadius
+ endState.bottomCornerRadius = endRadius
+ }
// We animate the opening window and delegate the view expansion to [this.controller].
val delegate = this.controller
val controller =
object : Controller by delegate {
+ override fun createAnimatorState(): TransitionAnimator.State {
+ if (isLaunching) return delegate.createAnimatorState()
+ val windowRadius = getWindowRadius(isExpandingFullyAbove)
+ return TransitionAnimator.State(
+ top = windowBounds.top,
+ bottom = windowBounds.bottom,
+ left = windowBounds.left,
+ right = windowBounds.right,
+ topCornerRadius = windowRadius,
+ bottomCornerRadius = windowRadius
+ )
+ }
+
override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
listener?.onTransitionAnimationStart()
@@ -795,6 +819,18 @@
)
}
+ private fun getWindowRadius(isExpandingFullyAbove: Boolean): Float {
+ return if (isExpandingFullyAbove) {
+ // Most of the time, expanding fully above the root view means
+ // expanding in full screen.
+ ScreenDecorationsUtils.getWindowCornerRadius(context)
+ } else {
+ // This usually means we are in split screen mode, so 2 out of 4
+ // corners will have a radius of 0.
+ 0f
+ }
+ }
+
private fun applyStateToWindow(
window: RemoteAnimationTarget,
state: TransitionAnimator.State,
@@ -842,20 +878,41 @@
windowCropF.bottom.roundToInt()
)
+ val windowAnimationDelay =
+ if (controller.isLaunching) {
+ TIMINGS.contentAfterFadeInDelay
+ } else {
+ TIMINGS.contentBeforeFadeOutDelay
+ }
+ val windowAnimationDuration =
+ if (controller.isLaunching) {
+ TIMINGS.contentAfterFadeInDuration
+ } else {
+ TIMINGS.contentBeforeFadeOutDuration
+ }
+ val windowProgress =
+ TransitionAnimator.getProgress(
+ TIMINGS,
+ linearProgress,
+ windowAnimationDelay,
+ windowAnimationDuration
+ )
+
// The alpha of the opening window. If it opens above the expandable, then it should
// fade in progressively. Otherwise, it should be fully opaque and will be progressively
// revealed as the window background color layer above the window fades out.
val alpha =
if (controller.isBelowAnimatingWindow) {
- val windowProgress =
- TransitionAnimator.getProgress(
- TIMINGS,
- linearProgress,
- TIMINGS.contentAfterFadeInDelay,
- TIMINGS.contentAfterFadeInDuration
+ if (controller.isLaunching) {
+ INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(
+ windowProgress
)
-
- INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation(windowProgress)
+ } else {
+ 1 -
+ INTERPOLATORS.contentBeforeFadeOutInterpolator.getInterpolation(
+ windowProgress
+ )
+ }
} else {
1f
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 9bf6b34..679c969 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -59,6 +59,13 @@
1.0f
)
}
+
+ internal fun checkReturnAnimationFrameworkFlag() {
+ check(returnAnimationFrameworkLibrary()) {
+ "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is " +
+ "disabled"
+ }
+ }
}
private val transitionContainerLocation = IntArray(2)
@@ -558,10 +565,4 @@
}
}
}
-
- private fun checkReturnAnimationFrameworkFlag() {
- check(returnAnimationFrameworkLibrary()) {
- "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is disabled"
- }
- }
}
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 04c4efb..fefe5a0 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
@@ -149,7 +149,6 @@
mUiEventLogger);
when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator);
when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker);
when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn(Float.MAX_VALUE);
@@ -193,11 +192,6 @@
2)).isTrue();
}
- private enum Direction {
- DOWN,
- UP,
- }
-
@Test
public void testSwipeUp_whenBouncerInitiallyShowing_reduceHeightWithExclusionRects() {
mTouchHandler.getTouchInitiationRegion(SCREEN_BOUNDS, mRegion,
@@ -210,7 +204,7 @@
SCREEN_HEIGHT_PX * MIN_BOUNCER_HEIGHT;
final int minAllowableBottom = SCREEN_HEIGHT_PX - Math.round(minBouncerHeight);
- expected.set(0, minAllowableBottom , SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX);
+ expected.set(0, minAllowableBottom, SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX);
assertThat(bounds).isEqualTo(expected);
@@ -278,69 +272,11 @@
}
/**
- * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount.
- */
- @DisableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING)
- @Test
- public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion() {
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-
- mTouchHandler.onSessionStart(mTouchSession);
- ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
- ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
- verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
-
- final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
-
- final float percent = .3f;
- final float distanceY = SCREEN_HEIGHT_PX * percent;
-
- // Swiping up near the top of the screen where the touch initiation region is.
- final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
- 0, distanceY, 0);
- final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
- 0, 0, 0);
-
- assertThat(gestureListener.onScroll(event1, event2, 0, distanceY)).isTrue();
-
- verify(mScrimController, never()).expand(any());
- }
-
- /**
- * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount.
- */
- @Test
- @EnableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING)
- public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion_directionFiltering() {
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-
- mTouchHandler.onSessionStart(mTouchSession);
- ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
- ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
- verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
-
- final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
-
- final float percent = .3f;
- final float distanceY = SCREEN_HEIGHT_PX * percent;
-
- // Swiping up near the top of the screen where the touch initiation region is.
- final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
- 0, distanceY, 0);
- final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
- 0, 0, 0);
-
- assertThat(gestureListener.onScroll(event1, event2, 0, distanceY)).isFalse();
-
- verify(mScrimController, never()).expand(any());
- }
-
- /**
- * Makes sure swiping down when bouncer initially hidden doesn't change the expansion amount.
+ * Makes sure swiping down doesn't change the expansion amount.
*/
@Test
@DisableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING)
- public void testSwipeDown_whenBouncerInitiallyHidden_doesNotSetExpansion() {
+ public void testSwipeDown_doesNotSetExpansion() {
mTouchHandler.onSessionStart(mTouchSession);
ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
@@ -401,34 +337,8 @@
final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
- verifyScroll(.3f, Direction.UP, false, gestureListener);
-
- // Ensure that subsequent gestures are treated as expanding even if the bouncer state
- // changes.
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
- verifyScroll(.7f, Direction.UP, false, gestureListener);
- }
-
- /**
- * Makes sure the expansion amount is proportional to scroll.
- */
- @Test
- public void testSwipeDown_setsCorrectExpansionAmount() {
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-
- mTouchHandler.onSessionStart(mTouchSession);
- ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
- ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
- verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture());
-
- final OnGestureListener gestureListener = gestureListenerCaptor.getValue();
-
- verifyScroll(.3f, Direction.DOWN, true, gestureListener);
-
- // Ensure that subsequent gestures are treated as collapsing even if the bouncer state
- // changes.
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
- verifyScroll(.7f, Direction.DOWN, true, gestureListener);
+ verifyScroll(.3f, gestureListener);
+ verifyScroll(.7f, gestureListener);
}
/**
@@ -493,25 +403,24 @@
verify(mCentralSurfaces, never()).awakenDreams();
}
- private void verifyScroll(float percent, Direction direction,
- boolean isBouncerInitiallyShowing, GestureDetector.OnGestureListener gestureListener) {
+ private void verifyScroll(float percent,
+ OnGestureListener gestureListener) {
final float distanceY = SCREEN_HEIGHT_PX * percent;
final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
- 0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0);
+ 0, SCREEN_HEIGHT_PX, 0);
final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
- 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0);
+ 0, SCREEN_HEIGHT_PX - distanceY, 0);
reset(mScrimController);
assertThat(gestureListener.onScroll(event1, event2, 0,
- direction == Direction.UP ? distanceY : -distanceY))
+ distanceY))
.isTrue();
// Ensure only called once
verify(mScrimController).expand(any());
- final float expansion = isBouncerInitiallyShowing ? percent : 1 - percent;
- final float dragDownAmount = event2.getY() - event1.getY();
+ final float expansion = 1 - percent;
// Ensure correct expansion passed in.
ShadeExpansionChangeEvent event =
@@ -529,7 +438,7 @@
final float expansion = 1 - swipeUpPercentage;
// The upward velocity is ignored.
final float velocityY = -1;
- swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
+ swipeToPosition(swipeUpPercentage, velocityY);
verify(mValueAnimatorCreator).create(eq(expansion),
eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
@@ -552,7 +461,7 @@
final float expansion = 1 - swipeUpPercentage;
// The downward velocity is ignored.
final float velocityY = 1;
- swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
+ swipeToPosition(swipeUpPercentage, velocityY);
verify(mValueAnimatorCreator).create(eq(expansion),
eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
@@ -573,57 +482,6 @@
}
/**
- * Tests that ending a downward swipe above the set threshold will continue the expansion,
- * but will not trigger logging of the DREAM_SWIPED event.
- */
- @Test
- public void testSwipeDownPositionAboveThreshold_expandsBouncer_doesNotLog() {
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
-
- final float swipeDownPercentage = .3f;
- // The downward velocity is ignored.
- final float velocityY = 1;
- swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
-
- verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
- eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
- verify(mValueAnimator, never()).addListener(any());
-
- verify(mFlingAnimationUtils).apply(eq(mValueAnimator),
- eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
- eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE),
- eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
- verify(mValueAnimator).start();
- verify(mUiEventLogger, never()).log(any());
- }
-
- /**
- * Tests that swiping down with a speed above the set threshold leads to bouncer collapsing
- * down.
- */
- @Test
- public void testSwipeDownVelocityAboveMin_collapsesBouncer() {
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
- when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0);
-
- // The ending position above the set threshold is ignored.
- final float swipeDownPercentage = .3f;
- final float velocityY = 1;
- swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY);
-
- verify(mValueAnimatorCreator).create(eq(swipeDownPercentage),
- eq(KeyguardBouncerConstants.EXPANSION_HIDDEN));
- verify(mValueAnimator, never()).addListener(any());
-
- verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator),
- eq(SCREEN_HEIGHT_PX * swipeDownPercentage),
- eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN),
- eq(velocityY), eq((float) SCREEN_HEIGHT_PX));
- verify(mValueAnimator).start();
- verify(mUiEventLogger, never()).log(any());
- }
-
- /**
* Tests that swiping up with a speed above the set threshold will continue the expansion.
*/
@Test
@@ -634,7 +492,7 @@
final float swipeUpPercentage = .3f;
final float expansion = 1 - swipeUpPercentage;
final float velocityY = -1;
- swipeToPosition(swipeUpPercentage, Direction.UP, velocityY);
+ swipeToPosition(swipeUpPercentage, velocityY);
verify(mValueAnimatorCreator).create(eq(expansion),
eq(KeyguardBouncerConstants.EXPANSION_VISIBLE));
@@ -654,26 +512,6 @@
verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_BOUNCER_FULLY_VISIBLE);
}
- /**
- * Ensures {@link CentralSurfaces}
- */
- @Test
- public void testInformBouncerShowingOnExpand() {
- swipeToPosition(1f, Direction.UP, 0);
- }
-
- /**
- * Ensures {@link CentralSurfaces}
- */
- @Test
- public void testInformBouncerHidingOnCollapse() {
- // Must swipe up to set initial state.
- swipeToPosition(1f, Direction.UP, 0);
- Mockito.clearInvocations(mCentralSurfaces);
-
- swipeToPosition(0f, Direction.DOWN, 0);
- }
-
@Test
public void testTouchSessionOnRemovedCalledTwice() {
mTouchHandler.onSessionStart(mTouchSession);
@@ -684,7 +522,7 @@
onRemovedCallbackCaptor.getValue().onRemoved();
}
- private void swipeToPosition(float percent, Direction direction, float velocityY) {
+ private void swipeToPosition(float percent, float velocityY) {
Mockito.clearInvocations(mTouchSession);
mTouchHandler.onSessionStart(mTouchSession);
ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor =
@@ -699,12 +537,12 @@
final float distanceY = SCREEN_HEIGHT_PX * percent;
final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
- 0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0);
+ 0, SCREEN_HEIGHT_PX, 0);
final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
- 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0);
+ 0, SCREEN_HEIGHT_PX - distanceY, 0);
assertThat(gestureListenerCaptor.getValue().onScroll(event1, event2, 0,
- direction == Direction.UP ? distanceY : -distanceY))
+ distanceY))
.isTrue();
final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java
index 27bffd0..11a4241 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java
@@ -18,8 +18,10 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import android.view.GestureDetector;
import android.view.MotionEvent;
@@ -28,7 +30,6 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -36,6 +37,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -49,66 +51,89 @@
CentralSurfaces mCentralSurfaces;
@Mock
- ShadeViewController mShadeViewController;
-
- @Mock
TouchHandler.TouchSession mTouchSession;
ShadeTouchHandler mTouchHandler;
+ @Captor
+ ArgumentCaptor<GestureDetector.OnGestureListener> mGestureListenerCaptor;
+ @Captor
+ ArgumentCaptor<InputChannelCompat.InputEventListener> mInputListenerCaptor;
+
private static final int TOUCH_HEIGHT = 20;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController,
- TOUCH_HEIGHT);
+
+ mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), TOUCH_HEIGHT);
+ }
+
+ // Verifies that a swipe down in the gesture region is captured by the shade touch handler.
+ @Test
+ public void testSwipeDown_captured() {
+ final boolean captured = swipe(Direction.DOWN);
+
+ assertThat(captured).isTrue();
+ }
+
+ // Verifies that a swipe in the upward direction is not catpured.
+ @Test
+ public void testSwipeUp_notCaptured() {
+ final boolean captured = swipe(Direction.UP);
+
+ // Motion events not captured as the swipe is going in the wrong direction.
+ assertThat(captured).isFalse();
+ }
+
+ // Verifies that a swipe down forwards captured touches to the shade window for handling.
+ @Test
+ public void testSwipeDown_sentToShadeWindow() {
+ swipe(Direction.DOWN);
+
+ // Both motion events are sent for the shade window to process.
+ verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any());
+ }
+
+ // Verifies that a swipe down is not forwarded to the shade window.
+ @Test
+ public void testSwipeUp_touchesNotSent() {
+ swipe(Direction.UP);
+
+ // Motion events are not sent for the shade window to process as the swipe is going in the
+ // wrong direction.
+ verify(mCentralSurfaces, never()).handleExternalShadeWindowTouch(any());
}
/**
- * Verify that touches aren't handled when the bouncer is showing.
+ * Simulates a swipe in the given direction and returns true if the touch was intercepted by the
+ * touch handler's gesture listener.
+ * <p>
+ * Swipe down starts from a Y coordinate of 0 and goes downward. Swipe up starts from the edge
+ * of the gesture region, {@link #TOUCH_HEIGHT}, and goes upward to 0.
*/
- @Test
- public void testInactiveOnBouncer() {
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
+ private boolean swipe(Direction direction) {
+ Mockito.clearInvocations(mTouchSession);
mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).pop();
+
+ verify(mTouchSession).registerGestureListener(mGestureListenerCaptor.capture());
+ verify(mTouchSession).registerInputListener(mInputListenerCaptor.capture());
+
+ final float startY = direction == Direction.UP ? TOUCH_HEIGHT : 0;
+ final float endY = direction == Direction.UP ? 0 : TOUCH_HEIGHT;
+
+ // Send touches to the input and gesture listener.
+ final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, startY, 0);
+ final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, endY, 0);
+ mInputListenerCaptor.getValue().onInputEvent(event1);
+ mInputListenerCaptor.getValue().onInputEvent(event2);
+ final boolean captured = mGestureListenerCaptor.getValue().onScroll(event1, event2, 0,
+ startY - endY);
+
+ return captured;
}
- /**
- * Make sure {@link ShadeTouchHandler}
- */
- @Test
- public void testTouchPilferingOnScroll() {
- final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class);
- final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class);
-
- final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor =
- ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
-
- mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture());
-
- assertThat(gestureListenerArgumentCaptor.getValue()
- .onScroll(motionEvent1, motionEvent2, 1, 1))
- .isTrue();
+ private enum Direction {
+ DOWN, UP,
}
-
- /**
- * Ensure touches are propagated to the {@link ShadeViewController}.
- */
- @Test
- public void testEventPropagation() {
- final MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
-
- final ArgumentCaptor<InputChannelCompat.InputEventListener>
- inputEventListenerArgumentCaptor =
- ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
-
- mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
- inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
- verify(mShadeViewController).handleExternalTouch(motionEvent);
- }
-
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index 76f15d2..b4b812d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -32,7 +32,9 @@
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notificationShadeWindowController
import com.android.systemui.testKosmos
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
@@ -68,8 +70,10 @@
keyguardTransitionInteractor = keyguardTransitionInteractor,
keyguardInteractor = keyguardInteractor,
systemSettings = fakeSettings,
+ notificationShadeWindowController = notificationShadeWindowController,
applicationScope = applicationCoroutineScope,
bgScope = applicationCoroutineScope,
+ mainDispatcher = testDispatcher,
)
.apply { start() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 41bc1dc..e2e5169 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.dreams;
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
@@ -28,6 +30,7 @@
import android.content.res.Resources;
import android.graphics.Region;
import android.os.Handler;
+import android.testing.TestableLooper.RunWithLooper;
import android.view.AttachedSurfaceControl;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
@@ -43,6 +46,7 @@
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback;
import com.android.systemui.complication.ComplicationHostViewController;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.statusbar.BlurUtils;
import org.junit.Before;
@@ -52,8 +56,11 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import kotlinx.coroutines.CoroutineDispatcher;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
+@RunWithLooper(setAsMainLooper = true)
public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
private static final int MAX_BURN_IN_OFFSET = 20;
private static final long BURN_IN_PROTECTION_UPDATE_INTERVAL = 10;
@@ -87,6 +94,9 @@
Handler mHandler;
@Mock
+ CoroutineDispatcher mDispatcher;
+
+ @Mock
BlurUtils mBlurUtils;
@Mock
@@ -103,6 +113,8 @@
@Mock
DreamOverlayStateController mStateController;
+ @Mock
+ KeyguardTransitionInteractor mKeyguardTransitionInteractor;
DreamOverlayContainerViewController mController;
@@ -115,6 +127,7 @@
when(mDreamOverlayContainerView.getViewRootImpl()).thenReturn(mViewRoot);
when(mDreamOverlayContainerView.getRootSurfaceControl())
.thenReturn(mAttachedSurfaceControl);
+ when(mKeyguardTransitionInteractor.isFinishedInStateWhere(any())).thenReturn(emptyFlow());
mController = new DreamOverlayContainerViewController(
mDreamOverlayContainerView,
@@ -124,6 +137,7 @@
mLowLightTransitionCoordinator,
mBlurUtils,
mHandler,
+ mDispatcher,
mResources,
MAX_BURN_IN_OFFSET,
BURN_IN_PROTECTION_UPDATE_INTERVAL,
@@ -131,7 +145,8 @@
mPrimaryBouncerCallbackInteractor,
mAnimationsController,
mStateController,
- mBouncerlessScrimController);
+ mBouncerlessScrimController,
+ mKeyguardTransitionInteractor);
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index b18a8ec..bdb0c9a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -17,35 +17,52 @@
import android.content.ComponentName
import android.content.Intent
-import android.os.RemoteException
import android.service.dreams.IDreamOverlay
import android.service.dreams.IDreamOverlayCallback
import android.service.dreams.IDreamOverlayClient
import android.service.dreams.IDreamOverlayClientCallback
+import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.WindowManagerImpl
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
import com.android.internal.logging.UiEventLogger
import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.SysuiTestCase
import com.android.systemui.ambient.touch.TouchMonitor
import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
+import com.android.systemui.ambient.touch.scrim.ScrimController
+import com.android.systemui.ambient.touch.scrim.ScrimManager
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.complication.ComplicationHostViewController
import com.android.systemui.complication.ComplicationLayoutEngine
import com.android.systemui.complication.dagger.ComplicationComponent
import com.android.systemui.dreams.complication.HideComplicationTouchHandler
import com.android.systemui.dreams.dagger.DreamOverlayComponent
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
import com.android.systemui.touch.TouchInsetManager
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -53,20 +70,24 @@
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-import org.mockito.invocation.InvocationOnMock
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
class DreamOverlayServiceTest : SysuiTestCase() {
private val mFakeSystemClock = FakeSystemClock()
private val mMainExecutor = FakeExecutor(mFakeSystemClock)
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
@Mock lateinit var mLifecycleOwner: DreamOverlayLifecycleOwner
- @Mock lateinit var mLifecycleRegistry: LifecycleRegistry
+ private lateinit var lifecycleRegistry: FakeLifecycleRegistry
- lateinit var mWindowParams: WindowManager.LayoutParams
+ private lateinit var mWindowParams: WindowManager.LayoutParams
@Mock lateinit var mDreamOverlayCallback: IDreamOverlayCallback
@@ -114,18 +135,33 @@
@Mock lateinit var mUiEventLogger: UiEventLogger
+ @Mock lateinit var mScrimManager: ScrimManager
+
+ @Mock lateinit var mScrimController: ScrimController
+
+ @Mock lateinit var mSystemDialogsCloser: SystemDialogsCloser
+
@Mock lateinit var mDreamOverlayCallbackController: DreamOverlayCallbackController
+ private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
+ private lateinit var communalRepository: FakeCommunalRepository
+
@Captor var mViewCaptor: ArgumentCaptor<View>? = null
- var mService: DreamOverlayService? = null
+ private lateinit var mService: DreamOverlayService
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
+
+ lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner)
+ bouncerRepository = kosmos.fakeKeyguardBouncerRepository
+ communalRepository = kosmos.fakeCommunalRepository
+
whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController())
.thenReturn(mDreamOverlayContainerViewController)
whenever(mComplicationComponent.getComplicationHostViewController())
.thenReturn(mComplicationHostViewController)
- whenever(mLifecycleOwner.registry).thenReturn(mLifecycleRegistry)
+ whenever(mLifecycleOwner.registry).thenReturn(lifecycleRegistry)
whenever(mComplicationComponentFactory.create(any(), any(), any(), any()))
.thenReturn(mComplicationComponent)
whenever(mComplicationComponent.getVisibilityController())
@@ -141,6 +177,7 @@
whenever(mAmbientTouchComponent.getTouchMonitor()).thenReturn(mTouchMonitor)
whenever(mDreamOverlayContainerViewController.containerView)
.thenReturn(mDreamOverlayContainerView)
+ whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController)
mWindowParams = WindowManager.LayoutParams()
mService =
DreamOverlayService(
@@ -154,24 +191,30 @@
mAmbientTouchComponentFactory,
mStateController,
mKeyguardUpdateMonitor,
+ mScrimManager,
+ kosmos.communalInteractor,
+ mSystemDialogsCloser,
mUiEventLogger,
mTouchInsetManager,
LOW_LIGHT_COMPONENT,
HOME_CONTROL_PANEL_DREAM_COMPONENT,
mDreamOverlayCallbackController,
+ kosmos.keyguardInteractor,
WINDOW_NAME
)
}
- @get:Throws(RemoteException::class)
- val client: IDreamOverlayClient
+ private val client: IDreamOverlayClient
get() {
- val proxy = mService!!.onBind(Intent())
+ mService.onCreate()
+ TestableLooper.get(this).processAllMessages()
+
+ val proxy = mService.onBind(Intent())
val overlay = IDreamOverlay.Stub.asInterface(proxy)
val callback = Mockito.mock(IDreamOverlayClientCallback::class.java)
overlay.getClient(callback)
val clientCaptor = ArgumentCaptor.forClass(IDreamOverlayClient::class.java)
- Mockito.verify(callback).onDreamOverlayClient(clientCaptor.capture())
+ verify(callback).onDreamOverlayClient(clientCaptor.capture())
return clientCaptor.value
}
@@ -187,9 +230,8 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Mockito.verify(mUiEventLogger)
- .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START)
- Mockito.verify(mUiEventLogger)
+ verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START)
+ verify(mUiEventLogger)
.log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
}
@@ -205,7 +247,7 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Mockito.verify(mWindowManager).addView(any(), any())
+ verify(mWindowManager).addView(any(), any())
}
// Validates that {@link DreamOverlayService} properly handles the case where the dream's
@@ -224,14 +266,14 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Mockito.verify(mWindowManager).addView(any(), any())
- Mockito.verify(mStateController).setOverlayActive(false)
- Mockito.verify(mStateController).setLowLightActive(false)
- Mockito.verify(mStateController).setEntryAnimationsFinished(false)
- Mockito.verify(mStateController, Mockito.never()).setOverlayActive(true)
- Mockito.verify(mUiEventLogger, Mockito.never())
+ verify(mWindowManager).addView(any(), any())
+ verify(mStateController).setOverlayActive(false)
+ verify(mStateController).setLowLightActive(false)
+ verify(mStateController).setEntryAnimationsFinished(false)
+ verify(mStateController, Mockito.never()).setOverlayActive(true)
+ verify(mUiEventLogger, Mockito.never())
.log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
- Mockito.verify(mDreamOverlayCallbackController, Mockito.never()).onStartDream()
+ verify(mDreamOverlayCallbackController, Mockito.never()).onStartDream()
}
@Test
@@ -246,7 +288,7 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Mockito.verify(mDreamOverlayContainerViewController).init()
+ verify(mDreamOverlayContainerViewController).init()
}
@Test
@@ -264,7 +306,7 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Mockito.verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView)
+ verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView)
}
@Test
@@ -279,7 +321,7 @@
true /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Truth.assertThat(mService!!.shouldShowComplications()).isTrue()
+ assertThat(mService.shouldShowComplications()).isTrue()
}
@Test
@@ -294,8 +336,8 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Truth.assertThat(mService!!.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT)
- Mockito.verify(mStateController).setLowLightActive(true)
+ assertThat(mService.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT)
+ verify(mStateController).setLowLightActive(true)
}
@Test
@@ -310,8 +352,8 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Truth.assertThat(mService!!.dreamComponent).isEqualTo(HOME_CONTROL_PANEL_DREAM_COMPONENT)
- Mockito.verify(mStateController).setHomeControlPanelActive(true)
+ assertThat(mService.dreamComponent).isEqualTo(HOME_CONTROL_PANEL_DREAM_COMPONENT)
+ verify(mStateController).setHomeControlPanelActive(true)
}
@Test
@@ -328,19 +370,19 @@
mMainExecutor.runAllReady()
// Verify view added.
- Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
+ verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
// Service destroyed.
- mService!!.onEndDream()
+ mService.onEndDream()
mMainExecutor.runAllReady()
// Verify view removed.
- Mockito.verify(mWindowManager).removeView(mViewCaptor!!.value)
+ verify(mWindowManager).removeView(mViewCaptor!!.value)
// Verify state correctly set.
- Mockito.verify(mStateController).setOverlayActive(false)
- Mockito.verify(mStateController).setLowLightActive(false)
- Mockito.verify(mStateController).setEntryAnimationsFinished(false)
+ verify(mStateController).setOverlayActive(false)
+ verify(mStateController).setLowLightActive(false)
+ verify(mStateController).setEntryAnimationsFinished(false)
}
@Test
@@ -373,7 +415,7 @@
// Schedule the endDream call in the middle of the startDream implementation, as any
// ordering is possible.
- Mockito.doAnswer { invocation: InvocationOnMock? ->
+ Mockito.doAnswer {
client.endDream()
null
}
@@ -409,37 +451,37 @@
mMainExecutor.runAllReady()
// Verify view added.
- Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
+ verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
// Service destroyed.
- mService!!.onDestroy()
+ mService.onDestroy()
mMainExecutor.runAllReady()
// Verify view removed.
- Mockito.verify(mWindowManager).removeView(mViewCaptor!!.value)
+ verify(mWindowManager).removeView(mViewCaptor!!.value)
// Verify state correctly set.
- Mockito.verify(mKeyguardUpdateMonitor).removeCallback(any())
- Mockito.verify(mLifecycleRegistry).currentState = Lifecycle.State.DESTROYED
- Mockito.verify(mStateController).setOverlayActive(false)
- Mockito.verify(mStateController).setLowLightActive(false)
- Mockito.verify(mStateController).setEntryAnimationsFinished(false)
+ verify(mKeyguardUpdateMonitor).removeCallback(any())
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ verify(mStateController).setOverlayActive(false)
+ verify(mStateController).setLowLightActive(false)
+ verify(mStateController).setEntryAnimationsFinished(false)
}
@Test
fun testDoNotRemoveViewOnDestroyIfOverlayNotStarted() {
// Service destroyed without ever starting dream.
- mService!!.onDestroy()
+ mService.onDestroy()
mMainExecutor.runAllReady()
// Verify no view is removed.
- Mockito.verify(mWindowManager, Mockito.never()).removeView(any())
+ verify(mWindowManager, Mockito.never()).removeView(any())
// Verify state still correctly set.
- Mockito.verify(mKeyguardUpdateMonitor).removeCallback(any())
- Mockito.verify(mLifecycleRegistry).currentState = Lifecycle.State.DESTROYED
- Mockito.verify(mStateController).setOverlayActive(false)
- Mockito.verify(mStateController).setLowLightActive(false)
+ verify(mKeyguardUpdateMonitor).removeCallback(any())
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+ verify(mStateController).setOverlayActive(false)
+ verify(mStateController).setLowLightActive(false)
}
@Test
@@ -447,7 +489,7 @@
val client = client
// Destroy the service.
- mService!!.onDestroy()
+ mService.onDestroy()
mMainExecutor.runAllReady()
// Inform the overlay service of dream starting.
@@ -458,15 +500,15 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Mockito.verify(mWindowManager, Mockito.never()).addView(any(), any())
+ verify(mWindowManager, Mockito.never()).addView(any(), any())
}
@Test
fun testNeverRemoveDecorViewIfNotAdded() {
// Service destroyed before dream started.
- mService!!.onDestroy()
+ mService.onDestroy()
mMainExecutor.runAllReady()
- Mockito.verify(mWindowManager, Mockito.never()).removeView(any())
+ verify(mWindowManager, Mockito.never()).removeView(any())
}
@Test
@@ -483,11 +525,11 @@
mMainExecutor.runAllReady()
// Verify that a new window is added.
- Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
+ verify(mWindowManager).addView(mViewCaptor!!.capture(), any())
val windowDecorView = mViewCaptor!!.value
// Assert that the overlay is not showing complications.
- Truth.assertThat(mService!!.shouldShowComplications()).isFalse()
+ assertThat(mService.shouldShowComplications()).isFalse()
Mockito.clearInvocations(mDreamOverlayComponent)
Mockito.clearInvocations(mAmbientTouchComponent)
Mockito.clearInvocations(mWindowManager)
@@ -504,16 +546,16 @@
mMainExecutor.runAllReady()
// Assert that the overlay is showing complications.
- Truth.assertThat(mService!!.shouldShowComplications()).isTrue()
+ assertThat(mService.shouldShowComplications()).isTrue()
// Verify that the old overlay window has been removed, and a new one created.
- Mockito.verify(mWindowManager).removeView(windowDecorView)
- Mockito.verify(mWindowManager).addView(any(), any())
+ verify(mWindowManager).removeView(windowDecorView)
+ verify(mWindowManager).addView(any(), any())
// Verify that new instances of overlay container view controller and overlay touch monitor
// are created.
- Mockito.verify(mDreamOverlayComponent).getDreamOverlayContainerViewController()
- Mockito.verify(mAmbientTouchComponent).getTouchMonitor()
+ verify(mDreamOverlayComponent).getDreamOverlayContainerViewController()
+ verify(mAmbientTouchComponent).getTouchMonitor()
}
@Test
@@ -528,15 +570,15 @@
true /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- mService!!.onWakeUp()
- Mockito.verify(mDreamOverlayContainerViewController).wakeUp()
- Mockito.verify(mDreamOverlayCallbackController).onWakeUp()
+ mService.onWakeUp()
+ verify(mDreamOverlayContainerViewController).wakeUp()
+ verify(mDreamOverlayCallbackController).onWakeUp()
}
@Test
fun testWakeUpBeforeStartDoesNothing() {
- mService!!.onWakeUp()
- Mockito.verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp()
+ mService.onWakeUp()
+ verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp()
}
@Test
@@ -554,8 +596,8 @@
val paramsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams::class.java)
// Verify that a new window is added.
- Mockito.verify(mWindowManager).addView(any(), paramsCaptor.capture())
- Truth.assertThat(
+ verify(mWindowManager).addView(any(), paramsCaptor.capture())
+ assertThat(
paramsCaptor.value.privateFlags and
WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS ==
WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
@@ -563,6 +605,254 @@
.isTrue()
}
+ // Tests that the bouncer closes when DreamOverlayService is told that the dream is coming to
+ // the front.
+ @Test
+ fun testBouncerRetractedWhenDreamComesToFront() {
+ val client = client
+
+ // Inform the overlay service of dream starting.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ true /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+
+ whenever(mDreamOverlayContainerViewController.isBouncerShowing()).thenReturn(true)
+ mService!!.onComeToFront()
+ verify(mScrimController).expand(any())
+ }
+
+ // Tests that glanceable hub is hidden when DreamOverlayService is told that the dream is
+ // coming to the front.
+ @Test
+ fun testGlanceableHubHiddenWhenDreamComesToFront() {
+ val client = client
+
+ // Inform the overlay service of dream starting.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ true /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+
+ mService!!.onComeToFront()
+ assertThat(communalRepository.currentScene.value).isEqualTo(CommunalScenes.Blank)
+ }
+
+ // Tests that system dialogs (e.g. notification shade) closes when DreamOverlayService is told
+ // that the dream is coming to the front.
+ @Test
+ fun testSystemDialogsClosedWhenDreamComesToFront() {
+ val client = client
+
+ // Inform the overlay service of dream starting.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ true /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+
+ mService!!.onComeToFront()
+ verify(mSystemDialogsCloser).closeSystemDialogs()
+ }
+
+ @Test
+ fun testLifecycle_createdAfterConstruction() {
+ mMainExecutor.runAllReady()
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.CREATED)
+ }
+
+ @Test
+ fun testLifecycle_resumedAfterDreamStarts() {
+ val client = client
+
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+ assertThat(lifecycleRegistry.mLifecycles)
+ .containsExactly(
+ Lifecycle.State.CREATED,
+ Lifecycle.State.STARTED,
+ Lifecycle.State.RESUMED
+ )
+ }
+
+ @Test
+ fun testLifecycle_destroyedAfterOnDestroy() {
+ val client = client
+
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+ mService.onDestroy()
+ mMainExecutor.runAllReady()
+ assertThat(lifecycleRegistry.mLifecycles)
+ .containsExactly(
+ Lifecycle.State.CREATED,
+ Lifecycle.State.STARTED,
+ Lifecycle.State.RESUMED,
+ Lifecycle.State.DESTROYED
+ )
+ }
+
+ @Test
+ fun testNotificationShadeShown_setsLifecycleState() {
+ val client = client
+
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ val callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
+ verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture())
+
+ // Notification shade opens.
+ callbackCaptor.value.onShadeExpandedChanged(true)
+ mMainExecutor.runAllReady()
+
+ // Lifecycle state goes from resumed back to started when the notification shade shows.
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ // Notification shade closes.
+ callbackCaptor.value.onShadeExpandedChanged(false)
+ mMainExecutor.runAllReady()
+
+ // Lifecycle state goes back to RESUMED.
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
+ fun testBouncerShown_setsLifecycleState() {
+ val client = client
+
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // Bouncer shows.
+ bouncerRepository.setPrimaryShow(true)
+ testScope.runCurrent()
+ mMainExecutor.runAllReady()
+
+ // Lifecycle state goes from resumed back to started when the notification shade shows.
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ // Bouncer closes.
+ bouncerRepository.setPrimaryShow(false)
+ testScope.runCurrent()
+ mMainExecutor.runAllReady()
+
+ // Lifecycle state goes back to RESUMED.
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
+ fun testCommunalVisible_setsLifecycleState() {
+ val client = client
+
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ val transitionState: MutableStateFlow<ObservableTransitionState> =
+ MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank))
+ communalRepository.setTransitionState(transitionState)
+
+ // Communal becomes visible.
+ transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal)
+ testScope.runCurrent()
+ mMainExecutor.runAllReady()
+
+ // Lifecycle state goes from resumed back to started when the notification shade shows.
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ // Communal closes.
+ transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Blank)
+ testScope.runCurrent()
+ mMainExecutor.runAllReady()
+
+ // Lifecycle state goes back to RESUMED.
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ // Verifies the dream's lifecycle
+ @Test
+ fun testLifecycleStarted_whenAnyOcclusion() {
+ val client = client
+
+ // Inform the overlay service of dream starting. Do not show dream complications.
+ client.startDream(
+ mWindowParams,
+ mDreamOverlayCallback,
+ DREAM_COMPONENT,
+ false /*shouldShowComplication*/
+ )
+ mMainExecutor.runAllReady()
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ val transitionState: MutableStateFlow<ObservableTransitionState> =
+ MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank))
+ communalRepository.setTransitionState(transitionState)
+
+ // Communal becomes visible.
+ transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal)
+ testScope.runCurrent()
+ mMainExecutor.runAllReady()
+
+ // Lifecycle state goes from resumed back to started when the notification shade shows.
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+ // Communal closes.
+ transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Blank)
+ testScope.runCurrent()
+ mMainExecutor.runAllReady()
+
+ // Lifecycle state goes back to RESUMED.
+ assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) {
+ val mLifecycles: MutableList<State> = ArrayList()
+
+ override var currentState: State
+ get() = mLifecycles[mLifecycles.size - 1]
+ set(state) {
+ mLifecycles.add(state)
+ }
+ }
+
companion object {
private val LOW_LIGHT_COMPONENT = ComponentName("package", "lowlight")
private val HOME_CONTROL_PANEL_DREAM_COMPONENT =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
index 29fbee0..e3c6dee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
@@ -108,7 +108,7 @@
mTouchHandler.onSessionStart(mTouchSession);
verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
- verify(mCentralSurfaces).handleDreamTouch(motionEvent);
+ verify(mCentralSurfaces).handleExternalShadeWindowTouch(motionEvent);
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt
similarity index 96%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt
index 805b4a8..855b6d0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt
@@ -44,14 +44,14 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
-class SeekableSliderHapticPluginTest : SysuiTestCase() {
+class SeekbarHapticPluginTest : SysuiTestCase() {
private val kosmos = Kosmos()
@Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var vibratorHelper: VibratorHelper
private val seekBar = SeekBar(mContext)
- private lateinit var plugin: SeekableSliderHapticPlugin
+ private lateinit var plugin: SeekbarHapticPlugin
@Before
fun setup() {
@@ -142,7 +142,7 @@
private fun createPlugin() {
plugin =
- SeekableSliderHapticPlugin(
+ SeekbarHapticPlugin(
vibratorHelper,
kosmos.fakeSystemClock,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt
new file mode 100644
index 0000000..88189db
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.slider
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SliderStateProducerTest : SysuiTestCase() {
+
+ private val eventProducer = SliderStateProducer()
+ private val eventFlow = eventProducer.produceEvents()
+
+ @Test
+ fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest {
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onStartTracking(/*fromUser =*/ true)
+
+ assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest)
+ }
+
+ @Test
+ fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest {
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onStopTracking(/*fromUser =*/ true)
+
+ assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest)
+ }
+
+ @Test
+ fun onStartTrackingProgram_noProgress_trackingTouchEventProduced() = runTest {
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onStartTracking(/*fromUser =*/ false)
+
+ assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, 0F), latest)
+ }
+
+ @Test
+ fun onStopTrackingProgram_noProgress_StoppedTrackingTouchEventProduced() = runTest {
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onStopTracking(/*fromUser =*/ false)
+
+ assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, 0F), latest)
+ }
+
+ @Test
+ fun onProgressChangeByUser_changeByUserEventProduced() = runTest {
+ val progress = 0.5f
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onProgressChanged(/*fromUser =*/ true, progress)
+
+ assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress), latest)
+ }
+
+ @Test
+ fun onProgressChangeByProgram_changeByProgramEventProduced() = runTest {
+ val progress = 0.5f
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onProgressChanged(/*fromUser =*/ false, progress)
+
+ assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress), latest)
+ }
+
+ @Test
+ fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced() = runTest {
+ val progress = 0.5f
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onProgressChanged(/*fromUser =*/ true, progress)
+ eventProducer.onStartTracking(/*fromUser =*/ true)
+
+ assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress), latest)
+ }
+
+ @Test
+ fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced() = runTest {
+ val progress = 0.5f
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onProgressChanged(/*fromUser =*/ true, progress)
+ eventProducer.onStopTracking(/*fromUser =*/ true)
+
+ assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, progress), latest)
+ }
+
+ @Test
+ fun onStartTrackingProgram_afterProgress_trackingProgramEventProduced() = runTest {
+ val progress = 0.5f
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onProgressChanged(/*fromUser =*/ false, progress)
+ eventProducer.onStartTracking(/*fromUser =*/ false)
+
+ assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress), latest)
+ }
+
+ @Test
+ fun onStopTrackingProgram_afterProgress_stopTrackingProgramEventProduced() = runTest {
+ val progress = 0.5f
+ val latest by collectLastValue(eventFlow)
+
+ eventProducer.onProgressChanged(/*fromUser =*/ false, progress)
+ eventProducer.onStopTracking(/*fromUser =*/ false)
+
+ assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress), latest)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
index db31ad5..4d69f0d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt
@@ -24,6 +24,7 @@
import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.google.common.truth.Truth
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,6 +42,14 @@
}
@Test
+ fun parameterizeSceneContainer() {
+ val result = parameterizeSceneContainerFlag()
+ Truth.assertThat(result).hasSize(2)
+ Truth.assertThat(result[0].mOverrides[FLAG_SCENE_CONTAINER]).isFalse()
+ Truth.assertThat(result[1].mOverrides[FLAG_SCENE_CONTAINER]).isTrue()
+ }
+
+ @Test
fun oneUnrelatedAndSceneContainer() {
val unrelatedFlag = FLAG_EXAMPLE_FLAG
val result = FlagsParameterization.allCombinationsOf(unrelatedFlag).andSceneContainer()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
similarity index 77%
rename from packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index b89ccef..a8da116 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -29,7 +29,6 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
@@ -37,12 +36,10 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
import android.app.IActivityManager;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
-import android.testing.AndroidTestingRunner;
+import android.platform.test.flag.junit.FlagsParameterization;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
import android.view.WindowManager;
@@ -50,51 +47,26 @@
import androidx.test.filters.SmallTest;
import com.android.internal.colorextraction.ColorExtractor;
-import com.android.keyguard.KeyguardSecurityModel;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository;
import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository;
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor;
import com.android.systemui.communal.domain.interactor.CommunalInteractor;
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue;
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor;
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.res.R;
import com.android.systemui.scene.FakeWindowRootViewComponent;
-import com.android.systemui.scene.data.repository.SceneContainerRepository;
-import com.android.systemui.scene.domain.interactor.SceneInteractor;
-import com.android.systemui.scene.shared.logger.SceneLogger;
import com.android.systemui.settings.UserTracker;
-import com.android.systemui.shade.data.repository.FakeShadeRepository;
-import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
-import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
-import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
-import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
-import com.android.systemui.user.domain.interactor.UserSwitcherInteractor;
import com.google.common.util.concurrent.MoreExecutors;
@@ -110,13 +82,13 @@
import java.util.List;
import java.util.concurrent.Executor;
-import kotlinx.coroutines.test.TestScope;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
-@RunWith(AndroidTestingRunner.class)
+@RunWith(ParameterizedAndroidJunit4.class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
-
@Mock private WindowManager mWindowManager;
@Mock private DozeParameters mDozeParameters;
@Spy private final NotificationShadeWindowView mNotificationShadeWindowView = spy(
@@ -128,29 +100,31 @@
@Mock private SysuiColorExtractor mColorExtractor;
@Mock private ColorExtractor.GradientColors mGradientColors;
@Mock private DumpManager mDumpManager;
- @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
@Mock private KeyguardStateController mKeyguardStateController;
@Mock private AuthController mAuthController;
@Mock private ShadeWindowLogger mShadeWindowLogger;
@Mock private SelectedUserInteractor mSelectedUserInteractor;
@Mock private UserTracker mUserTracker;
- @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper;
@Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
@Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener;
private final Executor mMainExecutor = MoreExecutors.directExecutor();
private final Executor mBackgroundExecutor = MoreExecutors.directExecutor();
private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
- private final TestScope mTestScope = mKosmos.getTestScope();
- private ShadeInteractor mShadeInteractor;
private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
private float mPreferredRefreshRate = -1;
- private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor;
- private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor;
- private ScreenOffAnimationController mScreenOffAnimationController;
private SysuiStatusBarStateController mStatusBarStateController;
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
+ }
+
+ public NotificationShadeWindowControllerImplTest(FlagsParameterization flags) {
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -164,71 +138,7 @@
when(mDozeParameters.getAlwaysOn()).thenReturn(true);
when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
- FakeKeyguardRepository keyguardRepository = new FakeKeyguardRepository();
- FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
- FakeShadeRepository shadeRepository = new FakeShadeRepository();
-
- mScreenOffAnimationController = mKosmos.getScreenOffAnimationController();
mStatusBarStateController = spy(mKosmos.getStatusBarStateController());
- PowerInteractor powerInteractor = mKosmos.getPowerInteractor();
-
- SceneInteractor sceneInteractor = new SceneInteractor(
- mTestScope.getBackgroundScope(),
- new SceneContainerRepository(
- mTestScope.getBackgroundScope(),
- mKosmos.getFakeSceneContainerConfig(),
- mKosmos.getSceneDataSource()),
- mock(SceneLogger.class),
- mKosmos.getDeviceUnlockedInteractor());
-
- FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository();
- KeyguardTransitionInteractor keyguardTransitionInteractor =
- mKosmos.getKeyguardTransitionInteractor();
- KeyguardInteractor keyguardInteractor = new KeyguardInteractor(
- keyguardRepository,
- new FakeCommandQueue(),
- powerInteractor,
- new FakeKeyguardBouncerRepository(),
- new ConfigurationInteractor(configurationRepository),
- shadeRepository,
- keyguardTransitionInteractor,
- () -> sceneInteractor,
- () -> mKosmos.getFromGoneTransitionInteractor(),
- () -> mKosmos.getSharedNotificationContainerInteractor(),
- mTestScope);
- CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor();
-
- mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor();
- mFromPrimaryBouncerTransitionInteractor =
- mKosmos.getFromPrimaryBouncerTransitionInteractor();
-
- DeviceEntryUdfpsInteractor deviceEntryUdfpsInteractor =
- mock(DeviceEntryUdfpsInteractor.class);
- when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false));
-
- mShadeInteractor = new ShadeInteractorImpl(
- mTestScope.getBackgroundScope(),
- mKosmos.getDeviceProvisioningInteractor(),
- new FakeDisableFlagsRepository(),
- mock(DozeParameters.class),
- keyguardRepository,
- keyguardTransitionInteractor,
- powerInteractor,
- new FakeUserSetupRepository(),
- mock(UserSwitcherInteractor.class),
- new ShadeInteractorLegacyImpl(
- mTestScope.getBackgroundScope(),
- keyguardRepository,
- new SharedNotificationContainerInteractor(
- configurationRepository,
- mContext,
- new ResourcesSplitShadeStateController(),
- keyguardInteractor,
- deviceEntryUdfpsInteractor,
- () -> mLargeScreenHeaderHelper),
- shadeRepository
- )
- );
mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(
mContext,
@@ -245,13 +155,13 @@
mColorExtractor,
mDumpManager,
mKeyguardStateController,
- mScreenOffAnimationController,
+ mKosmos.getScreenOffAnimationController(),
mAuthController,
- () -> mShadeInteractor,
+ mKosmos::getShadeInteractor,
mShadeWindowLogger,
() -> mSelectedUserInteractor,
mUserTracker,
- () -> communalInteractor) {
+ mKosmos::getCommunalInteractor) {
@Override
protected boolean isDebuggable() {
return false;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 96b2b7a..aa0ca18 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -24,7 +24,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.DozeStateModel
@@ -80,7 +80,7 @@
@JvmStatic
@Parameters(name = "{0}")
fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ return parameterizeSceneContainerFlag()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index 2ab934c..07c4b00 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -16,7 +16,7 @@
package com.android.systemui.shade.domain.startable
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
@@ -25,9 +25,8 @@
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
-import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
@@ -50,24 +49,42 @@
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class ShadeStartableTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val shadeInteractor = kosmos.shadeInteractor
- private val sceneInteractor = kosmos.sceneInteractor
- private val shadeExpansionStateManager = kosmos.shadeExpansionStateManager
- private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
- private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
- private val fakeConfigurationRepository = kosmos.fakeConfigurationRepository
- private val fakeSceneDataSource = kosmos.fakeSceneDataSource
+ private val shadeInteractor by lazy { kosmos.shadeInteractor }
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val shadeExpansionStateManager by lazy { kosmos.shadeExpansionStateManager }
+ private val fakeConfigurationRepository by lazy { kosmos.fakeConfigurationRepository }
+ private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
- private val underTest = kosmos.shadeStartable
+ private lateinit var underTest: ShadeStartable
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return parameterizeSceneContainerFlag()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
+ @Before
+ fun setup() {
+ underTest = kosmos.shadeStartable
+ }
@Test
fun hydrateShadeMode() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 2397de6..5312ad8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
@@ -65,6 +66,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
+@EnableSceneContainer
class ShadeSceneViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
similarity index 69%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index be5af88..fc9535c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -19,60 +19,32 @@
package com.android.systemui.statusbar
import android.animation.ObjectAnimator
-import android.testing.AndroidTestingRunner
+import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
-import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.jank.interactionJankMonitor
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.LargeScreenHeaderHelper
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl
-import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl
-import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
-import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
-import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
-import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
-import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
import com.android.systemui.util.kotlin.JavaAdapter
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
@@ -87,26 +59,34 @@
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-import org.mockito.Mockito.`when` as whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper
-class StatusBarStateControllerImplTest : SysuiTestCase() {
+class StatusBarStateControllerImplTest(flags: FlagsParameterization?) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private lateinit var shadeInteractor: ShadeInteractor
- private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
- private lateinit var fromPrimaryBouncerTransitionInteractor:
- FromPrimaryBouncerTransitionInteractor
+
private val mockDarkAnimator = mock<ObjectAnimator>()
- private val deviceEntryUdfpsInteractor = mock<DeviceEntryUdfpsInteractor>()
- private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>()
private lateinit var underTest: StatusBarStateControllerImpl
private lateinit var uiEventLogger: UiEventLoggerFake
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return parameterizeSceneContainerFlag()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -118,7 +98,7 @@
uiEventLogger,
kosmos.interactionJankMonitor,
JavaAdapter(testScope.backgroundScope),
- { shadeInteractor },
+ { kosmos.shadeInteractor },
{ kosmos.deviceUnlockedInteractor },
{ kosmos.sceneInteractor },
{ kosmos.keyguardClockInteractor },
@@ -127,59 +107,6 @@
return mockDarkAnimator
}
}
-
- val powerInteractor =
- PowerInteractor(FakePowerRepository(), FalsingCollectorFake(), mock(), underTest)
- val keyguardRepository = FakeKeyguardRepository()
- val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
- val featureFlags = FakeFeatureFlagsClassic()
- val shadeRepository = FakeShadeRepository()
- val configurationRepository = FakeConfigurationRepository()
- val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
- fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor
- fromPrimaryBouncerTransitionInteractor = kosmos.fromPrimaryBouncerTransitionInteractor
-
- val keyguardInteractor =
- KeyguardInteractor(
- keyguardRepository,
- FakeCommandQueue(),
- powerInteractor,
- FakeKeyguardBouncerRepository(),
- ConfigurationInteractor(configurationRepository),
- shadeRepository,
- keyguardTransitionInteractor,
- { kosmos.sceneInteractor },
- { kosmos.fromGoneTransitionInteractor },
- { kosmos.sharedNotificationContainerInteractor },
- testScope,
- )
-
- whenever(deviceEntryUdfpsInteractor.isUdfpsSupported).thenReturn(MutableStateFlow(false))
- shadeInteractor =
- ShadeInteractorImpl(
- testScope.backgroundScope,
- kosmos.deviceProvisioningInteractor,
- FakeDisableFlagsRepository(),
- mock(),
- keyguardRepository,
- keyguardTransitionInteractor,
- powerInteractor,
- FakeUserSetupRepository(),
- mock(),
- ShadeInteractorLegacyImpl(
- testScope.backgroundScope,
- keyguardRepository,
- SharedNotificationContainerInteractor(
- configurationRepository,
- mContext,
- ResourcesSplitShadeStateController(),
- keyguardInteractor,
- deviceEntryUdfpsInteractor,
- largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
- ),
- shadeRepository,
- )
- )
}
@Test
diff --git a/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
new file mode 100644
index 0000000..47fd78a
--- /dev/null
+++ b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:zAdjustment="top">
+
+ <translate
+ android:fromYDelta="0"
+ android:toYDelta="100%"
+ android:duration="@android:integer/config_shortAnimTime" />
+</set>
diff --git a/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
new file mode 100644
index 0000000..77edf58
--- /dev/null
+++ b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<!-- Animation for when a dock window at the bottom of the screen is entering. -->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:zAdjustment="top">
+
+ <translate android:fromYDelta="100%"
+ android:toYDelta="0"
+ android:startOffset="@android:integer/config_shortAnimTime"
+ android:duration="@android:integer/config_mediumAnimTime"/>
+</set>
diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
new file mode 100644
index 0000000..292e496
--- /dev/null
+++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/shortcut_helper_sheet_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/shortcut_helper_sheet"
+ style="@style/Widget.Material3.BottomSheet"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
+
+ <!-- Drag handle for accessibility -->
+ <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+ android:id="@+id/drag_handle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:textAppearance="?textAppearanceDisplayLarge"
+ android:background="?colorTertiaryContainer"
+ android:text="Shortcut Helper Content" />
+ </LinearLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index 291f8a2..546bf1c 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -72,4 +72,8 @@
<item name="android:textColor">@color/material_dynamic_secondary80</item>
</style>
+ <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
+ <item name="android:windowLightNavigationBar">false</item>
+ </style>
+
</resources>
diff --git a/packages/SystemUI/res/values-xlarge-land/config.xml b/packages/SystemUI/res/values-xlarge-land/config.xml
new file mode 100644
index 0000000..5e4304e
--- /dev/null
+++ b/packages/SystemUI/res/values-xlarge-land/config.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+
+<resources>
+ <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">0.8</item>
+</resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 4a73d85..19273ec 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1010,4 +1010,7 @@
<!-- Whether volume panel should use the large screen layout or not -->
<bool name="volume_panel_is_large_screen">false</bool>
+
+ <!-- The width of the shortcut helper container, as a fraction of the screen's width. -->
+ <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">1.0</item>
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9aff9fd..b8e78a4 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1718,6 +1718,11 @@
<!-- Accessibility label for available satellite connection [CHAR LIMIT=NONE] -->
<string name="accessibility_status_bar_satellite_available">Satellite, connection available</string>
+ <!-- Text displayed indicating that the user is connected to a satellite signal. -->
+ <string name="satellite_connected_carrier_text">Connected to satellite</string>
+ <!-- Text displayed indicating that the user is not connected to a satellite signal. -->
+ <string name="satellite_not_connected_carrier_text">Not connected to satellite</string>
+
<!-- Accessibility label for managed profile icon (not shown on screen) [CHAR LIMIT=NONE] -->
<string name="accessibility_managed_profile">Work profile</string>
@@ -1991,7 +1996,7 @@
<string name="keyboard_shortcut_clear_text">Clear search query</string>
<!-- The title for keyboard shortcut search list [CHAR LIMIT=25] -->
<string name="keyboard_shortcut_search_list_title">Keyboard Shortcuts</string>
- <!-- The hint for keyboard shortcut search list [CHAR LIMIT=25] -->
+ <!-- The hint for keyboard shortcut search list [CHAR LIMIT=50] -->
<string name="keyboard_shortcut_search_list_hint">Search shortcuts</string>
<!-- The description for no shortcuts results [CHAR LIMIT=25] -->
<string name="keyboard_shortcut_search_list_no_result">No shortcuts found</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 2c9006e..3d57111 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1654,4 +1654,28 @@
<style name="Theme.SystemUI.Dialog.StickyKeys" parent="@style/Theme.SystemUI.Dialog">
<item name="android:colorBackground">@color/transparent</item>
</style>
+
+ <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity">
+ <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
+ <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
+ <item name="android:activityOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
+ <item name="android:taskOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
+ </style>
+
+ <style name="ShortcutHelperThemeCommon" parent="@style/Theme.Material3.DynamicColors.DayNight">
+ <item name="android:windowAnimationStyle">@style/ShortcutHelperAnimation</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowLayoutInDisplayCutoutMode">always</item>
+ <item name="enableEdgeToEdge">true</item>
+ </style>
+
+ <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
+ <item name="android:windowLightNavigationBar">true</item>
+ </style>
</resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
index b9b8fbe..5647b0b 100644
--- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
@@ -19,6 +19,7 @@
import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ACTIVE_DATA_SUB_CHANGED;
import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ON_TELEPHONY_CAPABLE;
import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_REFRESH_CARRIER_INFO;
+import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_SATELLITE_CHANGED;
import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_SIM_ERROR_STATE_CHANGED;
import android.content.Context;
@@ -43,17 +44,22 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel;
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository;
import com.android.systemui.telephony.TelephonyListenerManager;
+import com.android.systemui.util.kotlin.JavaAdapter;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
+import kotlinx.coroutines.Job;
+
/**
* Controller that generates text including the carrier names and/or the status of all the SIM
* interfaces in the device. Through a callback, the updates can be retrieved either as a list or
@@ -77,10 +83,17 @@
protected KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private final CarrierTextManagerLogger mLogger;
private final WifiRepository mWifiRepository;
+ private final DeviceBasedSatelliteViewModel mDeviceBasedSatelliteViewModel;
+ private final JavaAdapter mJavaAdapter;
private final boolean[] mSimErrorState;
private final int mSimSlotsNumber;
@Nullable // Check for nullability before dispatching
private CarrierTextCallback mCarrierTextCallback;
+ @Nullable
+ private Job mSatelliteConnectionJob;
+
+ @Nullable private String mSatelliteCarrierText;
+
private final Context mContext;
private final TelephonyManager mTelephonyManager;
private final CharSequence mSeparator;
@@ -178,6 +191,8 @@
boolean showAirplaneMode,
boolean showMissingSim,
WifiRepository wifiRepository,
+ DeviceBasedSatelliteViewModel deviceBasedSatelliteViewModel,
+ JavaAdapter javaAdapter,
TelephonyManager telephonyManager,
TelephonyListenerManager telephonyListenerManager,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -192,6 +207,8 @@
mShowAirplaneMode = showAirplaneMode;
mShowMissingSim = showMissingSim;
mWifiRepository = wifiRepository;
+ mDeviceBasedSatelliteViewModel = deviceBasedSatelliteViewModel;
+ mJavaAdapter = javaAdapter;
mTelephonyManager = telephonyManager;
mSeparator = separator;
mTelephonyListenerManager = telephonyListenerManager;
@@ -282,6 +299,11 @@
mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
});
mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener);
+ cancelSatelliteCollectionJob(/* reason= */ "Starting new job");
+ mSatelliteConnectionJob =
+ mJavaAdapter.alwaysCollectFlow(
+ mDeviceBasedSatelliteViewModel.getCarrierText(),
+ this::onSatelliteCarrierTextChanged);
} else {
// Don't listen and clear out the text when the device isn't a phone.
mMainExecutor.execute(() -> callback.updateCarrierInfo(
@@ -294,6 +316,7 @@
mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
});
mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener);
+ cancelSatelliteCollectionJob(/* reason= */ "Stopping listening");
}
}
@@ -311,6 +334,12 @@
return mKeyguardUpdateMonitor.getFilteredSubscriptionInfo();
}
+ private void onSatelliteCarrierTextChanged(@Nullable String text) {
+ mLogger.logUpdateCarrierTextForReason(REASON_SATELLITE_CHANGED);
+ mSatelliteCarrierText = text;
+ updateCarrierText();
+ }
+
protected void updateCarrierText() {
Trace.beginSection("CarrierTextManager#updateCarrierText");
boolean allSimsMissing = true;
@@ -411,6 +440,12 @@
airplaneMode = true;
}
+ String currentSatelliteText = mSatelliteCarrierText;
+ if (currentSatelliteText != null) {
+ mLogger.logUsingSatelliteText(currentSatelliteText);
+ displayText = currentSatelliteText;
+ }
+
final CarrierTextCallbackInfo info = new CarrierTextCallbackInfo(
displayText,
carrierNames,
@@ -616,11 +651,20 @@
return list;
}
+ private void cancelSatelliteCollectionJob(String reason) {
+ Job job = mSatelliteConnectionJob;
+ if (job != null) {
+ job.cancel(new CancellationException(reason));
+ }
+ }
+
/** Injectable Buildeer for {@#link CarrierTextManager}. */
public static class Builder {
private final Context mContext;
private final String mSeparator;
private final WifiRepository mWifiRepository;
+ private final DeviceBasedSatelliteViewModel mDeviceBasedSatelliteViewModel;
+ private final JavaAdapter mJavaAdapter;
private final TelephonyManager mTelephonyManager;
private final TelephonyListenerManager mTelephonyListenerManager;
private final WakefulnessLifecycle mWakefulnessLifecycle;
@@ -637,6 +681,8 @@
Context context,
@Main Resources resources,
@Nullable WifiRepository wifiRepository,
+ DeviceBasedSatelliteViewModel deviceBasedSatelliteViewModel,
+ JavaAdapter javaAdapter,
TelephonyManager telephonyManager,
TelephonyListenerManager telephonyListenerManager,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -648,6 +694,8 @@
mSeparator = resources.getString(
com.android.internal.R.string.kg_text_message_separator);
mWifiRepository = wifiRepository;
+ mDeviceBasedSatelliteViewModel = deviceBasedSatelliteViewModel;
+ mJavaAdapter = javaAdapter;
mTelephonyManager = telephonyManager;
mTelephonyListenerManager = telephonyListenerManager;
mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -682,9 +730,20 @@
public CarrierTextManager build() {
mLogger.setLocation(mDebugLocation);
return new CarrierTextManager(
- mContext, mSeparator, mShowAirplaneMode, mShowMissingSim, mWifiRepository,
- mTelephonyManager, mTelephonyListenerManager, mWakefulnessLifecycle,
- mMainExecutor, mBgExecutor, mKeyguardUpdateMonitor, mLogger);
+ mContext,
+ mSeparator,
+ mShowAirplaneMode,
+ mShowMissingSim,
+ mWifiRepository,
+ mDeviceBasedSatelliteViewModel,
+ mJavaAdapter,
+ mTelephonyManager,
+ mTelephonyListenerManager,
+ mWakefulnessLifecycle,
+ mMainExecutor,
+ mBgExecutor,
+ mKeyguardUpdateMonitor,
+ mLogger);
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 8c51a4e..519622e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -42,7 +42,6 @@
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED;
-import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;
import android.annotation.AnyThread;
import android.annotation.MainThread;
@@ -1868,7 +1867,9 @@
@Override
public void onPostureChanged(@DevicePostureInt int posture) {
if (posture == DEVICE_POSTURE_OPENED) {
- mLogger.d("Posture changed to open - attempting to request active unlock");
+ mLogger.d("Posture changed to open - attempting to request active"
+ + " unlock and run face auth");
+ getFaceAuthInteractor().onDeviceUnfolded();
requestActiveUnlockFromWakeReason(PowerManager.WAKE_REASON_UNFOLD_DEVICE,
false);
}
@@ -2434,9 +2435,7 @@
updateFingerprintListeningState(BIOMETRIC_ACTION_START);
}
});
- if (mConfigFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) {
- mDevicePostureController.addCallback(mPostureCallback);
- }
+ mDevicePostureController.addCallback(mPostureCallback);
updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener);
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
index cb474d3..48fea55 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
@@ -77,6 +77,15 @@
)
}
+ fun logUsingSatelliteText(satelliteText: String) {
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ { str1 = satelliteText },
+ { "┣ updateCarrierText: using satellite text. text=$str1" },
+ )
+ }
+
/** De-structures the info object so that we don't have to generate new strings */
fun logCallbackSentFromUpdate(info: CarrierTextCallbackInfo) {
buffer.log(
@@ -129,6 +138,7 @@
const val REASON_ON_TELEPHONY_CAPABLE = 2
const val REASON_SIM_ERROR_STATE_CHANGED = 3
const val REASON_ACTIVE_DATA_SUB_CHANGED = 4
+ const val REASON_SATELLITE_CHANGED = 5
@Retention(AnnotationRetention.SOURCE)
@IntDef(
@@ -138,6 +148,7 @@
REASON_ON_TELEPHONY_CAPABLE,
REASON_SIM_ERROR_STATE_CHANGED,
REASON_ACTIVE_DATA_SUB_CHANGED,
+ REASON_SATELLITE_CHANGED,
]
)
annotation class CarrierTextRefreshReason
@@ -148,6 +159,7 @@
REASON_ON_TELEPHONY_CAPABLE -> "ON_TELEPHONY_CAPABLE"
REASON_SIM_ERROR_STATE_CHANGED -> "SIM_ERROR_STATE_CHANGED"
REASON_ACTIVE_DATA_SUB_CHANGED -> "ACTIVE_DATA_SUB_CHANGED"
+ REASON_SATELLITE_CHANGED -> "SATELLITE_CHANGED"
else -> "unknown"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
index 39e1c41..55ccaa6 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -33,12 +33,23 @@
* abstract fun bind(impl: FoobarStartable): CoreStartable
* </pre>
*
- * If your CoreStartable depends on different CoreStartables starting before it, use a
- * {@link com.android.systemui.startable.Dependencies} annotation to list out those dependencies.
+ * If your CoreStartable depends on different CoreStartables starting before it, you can specify
+ * another map binding listing out its dependencies:
+ * <pre>
+ * @Provides
+ * @IntoMap
+ * @Dependencies // Important! com.android.systemui.startable.Dependencies.
+ * @ClassKey(FoobarStartable::class)
+ * fun providesDeps(): Set<Class<out CoreStartable>> {
+ * return setOf(OtherStartable::class.java)
+ * }
+ * </pre>
+ *
*
* @see SystemUIApplication#startSystemUserServicesIfNeeded()
*/
public interface CoreStartable extends Dumpable {
+ String STARTABLE_DEPENDENCIES = "startable_dependencies";
/** Main entry point for implementations. Called shortly after SysUI startup. */
void start();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index ba869bd..4b07402 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -44,16 +44,15 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.process.ProcessWrapper;
import com.android.systemui.res.R;
-import com.android.systemui.startable.Dependencies;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.util.NotificationChannels;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayDeque;
-import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import java.util.StringJoiner;
import java.util.TreeMap;
@@ -300,6 +299,7 @@
int serviceIndex = 0;
do {
+ startedAny = false;
queue = nextQueue;
nextQueue = new ArrayDeque<>(startables.size());
@@ -307,9 +307,9 @@
Map.Entry<Class<?>, Provider<CoreStartable>> entry = queue.removeFirst();
Class<?> cls = entry.getKey();
- Dependencies dep = cls.getAnnotation(Dependencies.class);
- Class<?>[] deps = (dep == null ? null : dep.value());
- if (deps == null || startedStartables.containsAll(Arrays.asList(deps))) {
+ Set<Class<? extends CoreStartable>> deps =
+ mSysUIComponent.getStartableDependencies().get(cls);
+ if (deps == null || startedStartables.containsAll(deps)) {
String clsName = cls.getName();
int i = serviceIndex; // Copied to make lambda happy.
timeInitialization(
@@ -331,12 +331,12 @@
while (!nextQueue.isEmpty()) {
Map.Entry<Class<?>, Provider<CoreStartable>> entry = nextQueue.removeFirst();
Class<?> cls = entry.getKey();
- Dependencies dep = cls.getAnnotation(Dependencies.class);
- Class<?>[] deps = (dep == null ? null : dep.value());
+ Set<Class<? extends CoreStartable>> deps =
+ mSysUIComponent.getStartableDependencies().get(cls);
StringJoiner stringJoiner = new StringJoiner(", ");
- for (int i = 0; deps != null && i < deps.length; i++) {
- if (!startedStartables.contains(deps[i])) {
- stringJoiner.add(deps[i].getName());
+ for (Class<? extends CoreStartable> c : deps) {
+ if (!startedStartables.contains(c)) {
+ stringJoiner.add(c.getName());
}
}
Log.e(TAG, "Failed to start " + cls.getName()
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
index d0f08f5..85aeb27 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java
@@ -27,6 +27,7 @@
import android.view.MotionEvent;
import android.view.VelocityTracker;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.UiEvent;
@@ -94,13 +95,11 @@
private Boolean mCapture;
private Boolean mExpanded;
- private boolean mBouncerInitiallyShowing;
-
private TouchSession mTouchSession;
- private ValueAnimatorCreator mValueAnimatorCreator;
+ private final ValueAnimatorCreator mValueAnimatorCreator;
- private VelocityTrackerFactory mVelocityTrackerFactory;
+ private final VelocityTrackerFactory mVelocityTrackerFactory;
private final UiEventLogger mUiEventLogger;
@@ -118,17 +117,12 @@
private final GestureDetector.OnGestureListener mOnGestureListener =
new GestureDetector.SimpleOnGestureListener() {
@Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
float distanceY) {
if (mCapture == null) {
- mBouncerInitiallyShowing = mCentralSurfaces
- .map(CentralSurfaces::isBouncerShowing)
- .orElse(false);
-
if (Flags.dreamOverlayBouncerSwipeDirectionFiltering()) {
mCapture = Math.abs(distanceY) > Math.abs(distanceX)
- && ((distanceY < 0 && mBouncerInitiallyShowing)
- || (distanceY > 0 && !mBouncerInitiallyShowing));
+ && distanceY > 0;
} else {
// If the user scrolling favors a vertical direction, begin capturing
// scrolls.
@@ -146,13 +140,8 @@
return false;
}
- // Don't set expansion for downward scroll when the bouncer is hidden.
- if (!mBouncerInitiallyShowing && (e1.getY() < e2.getY())) {
- return true;
- }
-
- // Don't set expansion for upward scroll when the bouncer is shown.
- if (mBouncerInitiallyShowing && (e1.getY() > e2.getY())) {
+ // Don't set expansion for downward scroll.
+ if (e1.getY() < e2.getY()) {
return true;
}
@@ -176,8 +165,7 @@
final float dragDownAmount = e2.getY() - e1.getY();
final float screenTravelPercentage = Math.abs(e1.getY() - e2.getY())
/ mTouchSession.getBounds().height();
- setPanelExpansion(mBouncerInitiallyShowing
- ? screenTravelPercentage : 1 - screenTravelPercentage);
+ setPanelExpansion(1 - screenTravelPercentage);
return true;
}
};
@@ -223,9 +211,9 @@
LockPatternUtils lockPatternUtils,
UserTracker userTracker,
@Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING)
- FlingAnimationUtils flingAnimationUtils,
+ FlingAnimationUtils flingAnimationUtils,
@Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING)
- FlingAnimationUtils flingAnimationUtilsClosing,
+ FlingAnimationUtils flingAnimationUtilsClosing,
@Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage,
@Named(BouncerSwipeModule.MIN_BOUNCER_ZONE_SCREEN_PERCENTAGE) float minRegionPercentage,
UiEventLogger uiEventLogger) {
@@ -247,17 +235,13 @@
public void getTouchInitiationRegion(Rect bounds, Region region, Rect exclusionRect) {
final int width = bounds.width();
final int height = bounds.height();
- final float minBouncerHeight = height * mMinBouncerZoneScreenPercentage;
final int minAllowableBottom = Math.round(height * (1 - mMinBouncerZoneScreenPercentage));
- final boolean isBouncerShowing =
- mCentralSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false);
- final Rect normalRegion = isBouncerShowing
- ? new Rect(0, 0, width, Math.round(height * mBouncerZoneScreenPercentage))
- : new Rect(0, Math.round(height * (1 - mBouncerZoneScreenPercentage)),
- width, height);
+ final Rect normalRegion = new Rect(0,
+ Math.round(height * (1 - mBouncerZoneScreenPercentage)),
+ width, height);
- if (!isBouncerShowing && exclusionRect != null) {
+ if (exclusionRect != null) {
int lowestBottom = Math.min(Math.max(0, exclusionRect.bottom), minAllowableBottom);
normalRegion.top = Math.max(normalRegion.top, lowestBottom);
}
@@ -322,8 +306,7 @@
: KeyguardBouncerConstants.EXPANSION_HIDDEN;
// Log the swiping up to show Bouncer event.
- if (!mBouncerInitiallyShowing
- && expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
+ if (expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
mUiEventLogger.log(DreamEvent.DREAM_SWIPED);
}
@@ -335,17 +318,15 @@
}
}
- private ValueAnimator createExpansionAnimator(float targetExpansion, float expansionHeight) {
+ private ValueAnimator createExpansionAnimator(float targetExpansion) {
final ValueAnimator animator =
mValueAnimatorCreator.create(mCurrentExpansion, targetExpansion);
animator.addUpdateListener(
animation -> {
float expansionFraction = (float) animation.getAnimatedValue();
- float dragDownAmount = expansionFraction * expansionHeight;
setPanelExpansion(expansionFraction);
});
- if (!mBouncerInitiallyShowing
- && targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
+ if (targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
animator.addListener(
new AnimatorListenerAdapter() {
@Override
@@ -381,8 +362,7 @@
final float viewHeight = mTouchSession.getBounds().height();
final float currentHeight = viewHeight * mCurrentExpansion;
final float targetHeight = viewHeight * expansion;
- final float expansionHeight = targetHeight - currentHeight;
- final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight);
+ final ValueAnimator animator = createExpansionAnimator(expansion);
if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
// Hides the bouncer, i.e., fully expands the space above the bouncer.
mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity,
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
index 9ef9938..9c7fc9d 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java
@@ -23,7 +23,8 @@
import android.view.GestureDetector;
import android.view.MotionEvent;
-import com.android.systemui.shade.ShadeViewController;
+import androidx.annotation.NonNull;
+
import com.android.systemui.statusbar.phone.CentralSurfaces;
import java.util.Optional;
@@ -37,29 +38,34 @@
*/
public class ShadeTouchHandler implements TouchHandler {
private final Optional<CentralSurfaces> mSurfaces;
- private final ShadeViewController mShadeViewController;
private final int mInitiationHeight;
+ /**
+ * Tracks whether or not we are capturing a given touch. Will be null before and after a touch.
+ */
+ private Boolean mCapture;
+
@Inject
ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces,
- ShadeViewController shadeViewController,
@Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) {
mSurfaces = centralSurfaces;
- mShadeViewController = shadeViewController;
mInitiationHeight = initiationHeight;
}
@Override
public void onSessionStart(TouchSession session) {
- if (mSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) {
+ if (mSurfaces.isEmpty()) {
session.pop();
return;
}
- session.registerInputListener(ev -> {
- mShadeViewController.handleExternalTouch((MotionEvent) ev);
+ session.registerCallback(() -> mCapture = null);
+ session.registerInputListener(ev -> {
if (ev instanceof MotionEvent) {
+ if (mCapture != null && mCapture) {
+ mSurfaces.get().handleExternalShadeWindowTouch((MotionEvent) ev);
+ }
if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
session.pop();
}
@@ -68,15 +74,25 @@
session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() {
@Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
float distanceY) {
- return true;
+ if (mCapture == null) {
+ // Only capture swipes that are going downwards.
+ mCapture = Math.abs(distanceY) > Math.abs(distanceX) && distanceY < 0;
+ if (mCapture) {
+ // Send the initial touches over, as the input listener has already
+ // processed these touches.
+ mSurfaces.get().handleExternalShadeWindowTouch(e1);
+ mSurfaces.get().handleExternalShadeWindowTouch(e2);
+ }
+ }
+ return mCapture;
}
@Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
float velocityY) {
- return true;
+ return mCapture;
}
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
index 59b59bf..7d4ba84 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
@@ -39,6 +39,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.shared.model.DevicePosture
import com.android.systemui.res.R
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -68,6 +69,8 @@
/** The info of current available camera. */
val cameraInfo: StateFlow<CameraInfo?>
+
+ val supportedPostures: List<DevicePosture>
}
/** Describes a biometric sensor */
@@ -188,6 +191,15 @@
initialValue = if (cameraInfoList.isNotEmpty()) cameraInfoList[0] else null
)
+ private val supportedPosture =
+ applicationContext.resources.getInteger(R.integer.config_face_auth_supported_posture)
+ override val supportedPostures: List<DevicePosture> =
+ if (supportedPosture == 0) {
+ DevicePosture.entries
+ } else {
+ listOf(DevicePosture.toPosture(supportedPosture))
+ }
+
private val defaultSensorLocation: StateFlow<Point?> =
cameraInfo
.map { it?.cameraLocation }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index 5a174b9..f437032 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -17,6 +17,7 @@
package com.android.systemui.communal
import android.provider.Settings
+import android.service.dreams.Flags.dreamTracksFocus
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.CoreStartable
import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -25,11 +26,13 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dock.DockManager
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.util.kotlin.emitOnStart
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -37,15 +40,18 @@
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* A [CoreStartable] responsible for automatically navigating between communal scenes when certain
@@ -61,8 +67,10 @@
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val keyguardInteractor: KeyguardInteractor,
private val systemSettings: SystemSettings,
+ private val notificationShadeWindowController: NotificationShadeWindowController,
@Application private val applicationScope: CoroutineScope,
@Background private val bgScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
) : CoreStartable {
private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT
@@ -134,6 +142,16 @@
}
}
}
+
+ if (dreamTracksFocus()) {
+ bgScope.launch {
+ communalInteractor.isIdleOnCommunal.collectLatest {
+ withContext(mainDispatcher) {
+ notificationShadeWindowController.setGlanceableHubShowing(it)
+ }
+ }
+ }
+ }
}
private suspend fun determineSceneAfterTransition(
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 804c8c4..1dd3722 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -25,6 +25,7 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
import com.android.systemui.people.PeopleProvider;
+import com.android.systemui.startable.Dependencies;
import com.android.systemui.statusbar.NotificationInsetsModule;
import com.android.systemui.statusbar.QsFrameTranslateModule;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -47,6 +48,7 @@
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import javax.inject.Provider;
@@ -160,6 +162,11 @@
@PerUser Map<Class<?>, Provider<CoreStartable>> getPerUserStartables();
/**
+ * Returns {@link CoreStartable} dependencies if there are any.
+ */
+ @Dependencies Map<Class<?>, Set<Class<? extends CoreStartable>>> getStartableDependencies();
+
+ /**
* Member injection into the supplied argument.
*/
void inject(SystemUIAppComponentFactoryBase factory);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 18498cb..3462164 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -30,6 +30,7 @@
import com.android.systemui.BootCompleteCache;
import com.android.systemui.BootCompleteCacheImpl;
import com.android.systemui.CameraProtectionModule;
+import com.android.systemui.CoreStartable;
import com.android.systemui.SystemUISecondaryUserService;
import com.android.systemui.accessibility.AccessibilityModule;
import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule;
@@ -106,6 +107,7 @@
import com.android.systemui.shade.transition.LargeScreenShadeInterpolatorImpl;
import com.android.systemui.shared.condition.Monitor;
import com.android.systemui.smartspace.dagger.SmartspaceModule;
+import com.android.systemui.startable.Dependencies;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -163,11 +165,14 @@
import dagger.Provides;
import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
+import dagger.multibindings.Multibinds;
import kotlinx.coroutines.CoroutineScope;
import java.util.Collections;
+import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.Executor;
import javax.inject.Named;
@@ -271,6 +276,10 @@
})
public abstract class SystemUIModule {
+ @Multibinds
+ @Dependencies
+ abstract Map<Class<?>, Set<Class<? extends CoreStartable>>> startableDependencyMap();
+
@Binds
abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache);
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt
index 7733de4..a32b2aa 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt
@@ -68,6 +68,7 @@
fun onPrimaryBouncerUserInput()
fun onAccessibilityAction()
fun onWalletLaunched()
+ fun onDeviceUnfolded()
/** Whether face auth is considered class 3 */
fun isFaceAuthStrong(): Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt
index 65f3eb7..6629f6e 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt
@@ -66,4 +66,5 @@
override fun onPrimaryBouncerUserInput() {}
override fun onAccessibilityAction() {}
override fun onWalletLaunched() = Unit
+ override fun onDeviceUnfolded() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
index 03819ed..6c6683a 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt
@@ -37,6 +37,7 @@
import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.DevicePosture
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
@@ -248,6 +249,12 @@
}
}
+ override fun onDeviceUnfolded() {
+ if (facePropertyRepository.supportedPostures.contains(DevicePosture.OPENED)) {
+ runFaceAuth(FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED, true)
+ }
+ }
+
override fun registerListener(listener: FaceAuthenticationListener) {
listeners.add(listener)
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 5a036b1..0c2709e 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -22,6 +22,7 @@
import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM;
import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.animation.Animator;
import android.content.res.Resources;
@@ -40,6 +41,8 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.dreams.dagger.DreamOverlayModule;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.res.R;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.statusbar.BlurUtils;
@@ -50,6 +53,8 @@
import javax.inject.Inject;
import javax.inject.Named;
+import kotlinx.coroutines.CoroutineDispatcher;
+
/**
* View controller for {@link DreamOverlayContainerView}.
*/
@@ -62,6 +67,7 @@
private final DreamOverlayAnimationsController mDreamOverlayAnimationsController;
private final DreamOverlayStateController mStateController;
private final LowLightTransitionCoordinator mLowLightTransitionCoordinator;
+ private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final ComplicationHostViewController mComplicationHostViewController;
@@ -81,6 +87,7 @@
// Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates).
private final Handler mHandler;
+ private final CoroutineDispatcher mMainDispatcher;
private final int mDreamOverlayMaxTranslationY;
private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
@@ -88,6 +95,7 @@
private boolean mBouncerAnimating;
private boolean mWakingUpFromSwipe;
+ private boolean mAnyBouncerShowing;
private final BouncerlessScrimController mBouncerlessScrimController;
@@ -170,6 +178,7 @@
LowLightTransitionCoordinator lowLightTransitionCoordinator,
BlurUtils blurUtils,
@Main Handler handler,
+ @Main CoroutineDispatcher mainDispatcher,
@Main Resources resources,
@Named(DreamOverlayModule.MAX_BURN_IN_OFFSET) int maxBurnInOffset,
@Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long
@@ -178,7 +187,8 @@
PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
DreamOverlayAnimationsController animationsController,
DreamOverlayStateController stateController,
- BouncerlessScrimController bouncerlessScrimController) {
+ BouncerlessScrimController bouncerlessScrimController,
+ KeyguardTransitionInteractor keyguardTransitionInteractor) {
super(containerView);
mDreamOverlayContentView = contentView;
mStatusBarViewController = statusBarViewController;
@@ -190,6 +200,8 @@
mBouncerlessScrimController = bouncerlessScrimController;
mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
+ mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+
mComplicationHostViewController = complicationHostViewController;
mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize(
R.dimen.dream_overlay_y_offset);
@@ -200,6 +212,7 @@
ViewGroup.LayoutParams.MATCH_PARENT));
mHandler = handler;
+ mMainDispatcher = mainDispatcher;
mMaxBurnInOffset = maxBurnInOffset;
mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval;
mMillisUntilFullJitter = millisUntilFullJitter;
@@ -225,6 +238,12 @@
mView.getRootSurfaceControl().setTouchableRegion(emptyRegion);
emptyRegion.recycle();
+ collectFlow(
+ mView,
+ mKeyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState),
+ isFinished -> mAnyBouncerShowing = isFinished,
+ mMainDispatcher);
+
// Start dream entry animations. Skip animations for low light clock.
if (!mStateController.isLowLightActive()) {
// If this is transitioning from the low light dream to the user dream, the overlay
@@ -246,6 +265,10 @@
return mView;
}
+ boolean isBouncerShowing() {
+ return mAnyBouncerShowing;
+ }
+
private void updateBurnInOffsets() {
// Make sure the offset starts at zero, to avoid a big jump in the overlay when it first
// appears.
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 1135afe..a9ef531 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -19,9 +19,11 @@
import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE;
import static com.android.systemui.dreams.dagger.DreamModule.DREAM_TOUCH_INSET_MANAGER;
import static com.android.systemui.dreams.dagger.DreamModule.HOME_CONTROL_PANEL_DREAM_COMPONENT;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.util.Log;
import android.view.View;
@@ -34,7 +36,10 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.LifecycleService;
+import androidx.lifecycle.ServiceLifecycleDispatcher;
import androidx.lifecycle.ViewModelStore;
import com.android.dream.lowlight.dagger.LowLightDreamModule;
@@ -45,15 +50,21 @@
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.ambient.touch.TouchMonitor;
import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent;
+import com.android.systemui.ambient.touch.scrim.ScrimManager;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
+import com.android.systemui.communal.shared.model.CommunalScenes;
import com.android.systemui.complication.Complication;
import com.android.systemui.complication.dagger.ComplicationComponent;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Named;
@@ -63,7 +74,8 @@
* dream reaches directly out to the service with a Window reference (via LayoutParams), which the
* service uses to insert its own child Window into the dream's parent Window.
*/
-public class DreamOverlayService extends android.service.dreams.DreamOverlayService {
+public class DreamOverlayService extends android.service.dreams.DreamOverlayService implements
+ LifecycleOwner {
private static final String TAG = "DreamOverlayService";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -76,6 +88,7 @@
private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
private final DreamOverlayCallbackController mDreamOverlayCallbackController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final ScrimManager mScrimManager;
@Nullable
private final ComponentName mLowLightDreamComponent;
@Nullable
@@ -93,6 +106,21 @@
// True if the service has been destroyed.
private boolean mDestroyed = false;
+ /**
+ * True if the notification shade is open.
+ */
+ private boolean mShadeExpanded = false;
+
+ /**
+ * True if any part of the glanceable hub is visible.
+ */
+ private boolean mCommunalVisible = false;
+
+ /**
+ * True if the primary bouncer is visible.
+ */
+ private boolean mBouncerShowing = false;
+
private final ComplicationComponent mComplicationComponent;
private final AmbientTouchComponent mAmbientTouchComponent;
@@ -102,27 +130,72 @@
private final DreamOverlayComponent mDreamOverlayComponent;
- private final DreamOverlayLifecycleOwner mLifecycleOwner;
+ /**
+ * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch
+ * handling, should be active. It will automatically be paused when the dream overlay is hidden
+ * while dreaming, such as when the notification shade, bouncer, or glanceable hub are visible.
+ */
private final LifecycleRegistry mLifecycleRegistry;
+ /**
+ * Drives the lifecycle exposed by this service's {@link #getLifecycle()}.
+ * <p>
+ * Used to mimic a {@link LifecycleService}, though we do not update the lifecycle in
+ * {@link #onBind(Intent)} since it's final in the base class.
+ */
+ private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this);
+
private TouchMonitor mTouchMonitor;
+ private final CommunalInteractor mCommunalInteractor;
+
+ private final SystemDialogsCloser mSystemDialogsCloser;
+
private final KeyguardUpdateMonitorCallback mKeyguardCallback =
new KeyguardUpdateMonitorCallback() {
@Override
public void onShadeExpandedChanged(boolean expanded) {
mExecutor.execute(() -> {
- if (getCurrentStateLocked() != Lifecycle.State.RESUMED
- && getCurrentStateLocked() != Lifecycle.State.STARTED) {
+ if (mShadeExpanded == expanded) {
return;
}
+ mShadeExpanded = expanded;
- setCurrentStateLocked(
- expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED);
+ updateLifecycleStateLocked();
});
}
};
+ private final Consumer<Boolean> mCommunalVisibleConsumer = new Consumer<>() {
+ @Override
+ public void accept(Boolean communalVisible) {
+ mExecutor.execute(() -> {
+ if (mCommunalVisible == communalVisible) {
+ return;
+ }
+
+ mCommunalVisible = communalVisible;
+
+ updateLifecycleStateLocked();
+ });
+ }
+ };
+
+ private final Consumer<Boolean> mBouncerShowingConsumer = new Consumer<>() {
+ @Override
+ public void accept(Boolean bouncerShowing) {
+ mExecutor.execute(() -> {
+ if (mBouncerShowing == bouncerShowing) {
+ return;
+ }
+
+ mBouncerShowing = bouncerShowing;
+
+ updateLifecycleStateLocked();
+ });
+ }
+ };
+
private final DreamOverlayStateController.Callback mExitAnimationFinishedCallback =
new DreamOverlayStateController.Callback() {
@Override
@@ -168,19 +241,24 @@
AmbientTouchComponent.Factory ambientTouchComponentFactory,
DreamOverlayStateController stateController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
+ ScrimManager scrimManager,
+ CommunalInteractor communalInteractor,
+ SystemDialogsCloser systemDialogsCloser,
UiEventLogger uiEventLogger,
@Named(DREAM_TOUCH_INSET_MANAGER) TouchInsetManager touchInsetManager,
@Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
- ComponentName lowLightDreamComponent,
+ ComponentName lowLightDreamComponent,
@Nullable @Named(HOME_CONTROL_PANEL_DREAM_COMPONENT)
- ComponentName homeControlPanelDreamComponent,
+ ComponentName homeControlPanelDreamComponent,
DreamOverlayCallbackController dreamOverlayCallbackController,
+ KeyguardInteractor keyguardInteractor,
@Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
super(executor);
mContext = context;
mExecutor = executor;
mWindowManager = windowManager;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ mScrimManager = scrimManager;
mLowLightDreamComponent = lowLightDreamComponent;
mHomeControlPanelDreamComponent = homeControlPanelDreamComponent;
mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
@@ -188,6 +266,8 @@
mUiEventLogger = uiEventLogger;
mDreamOverlayCallbackController = dreamOverlayCallbackController;
mWindowTitle = windowTitle;
+ mCommunalInteractor = communalInteractor;
+ mSystemDialogsCloser = systemDialogsCloser;
final ViewModelStore viewModelStore = new ViewModelStore();
final Complication.Host host =
@@ -203,10 +283,32 @@
new HashSet<>(Arrays.asList(
mDreamComplicationComponent.getHideComplicationTouchHandler(),
mDreamOverlayComponent.getCommunalTouchHandler())));
- mLifecycleOwner = lifecycleOwner;
- mLifecycleRegistry = mLifecycleOwner.getRegistry();
+ mLifecycleRegistry = lifecycleOwner.getRegistry();
- mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED));
+ mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED));
+
+ collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
+ mCommunalVisibleConsumer);
+ collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
+ mBouncerShowingConsumer);
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return mDispatcher.getLifecycle();
+ }
+
+ @Override
+ public void onCreate() {
+ mDispatcher.onServicePreSuperOnCreate();
+ super.onCreate();
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ mDispatcher.onServicePreSuperOnStart();
+ super.onStart(intent, startId);
}
@Override
@@ -214,19 +316,20 @@
mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
mExecutor.execute(() -> {
- setCurrentStateLocked(Lifecycle.State.DESTROYED);
+ setLifecycleStateLocked(Lifecycle.State.DESTROYED);
resetCurrentDreamOverlayLocked();
mDestroyed = true;
});
+ mDispatcher.onServicePreSuperOnDestroy();
super.onDestroy();
}
@Override
public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
- setCurrentStateLocked(Lifecycle.State.STARTED);
+ setLifecycleStateLocked(Lifecycle.State.STARTED);
mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
@@ -256,7 +359,7 @@
return;
}
- setCurrentStateLocked(Lifecycle.State.RESUMED);
+ setLifecycleStateLocked(Lifecycle.State.RESUMED);
mStateController.setOverlayActive(true);
final ComponentName dreamComponent = getDreamComponent();
mStateController.setLowLightActive(
@@ -276,14 +379,27 @@
resetCurrentDreamOverlayLocked();
}
- private Lifecycle.State getCurrentStateLocked() {
+ private Lifecycle.State getLifecycleStateLocked() {
return mLifecycleRegistry.getCurrentState();
}
- private void setCurrentStateLocked(Lifecycle.State state) {
+ private void setLifecycleStateLocked(Lifecycle.State state) {
mLifecycleRegistry.setCurrentState(state);
}
+ private void updateLifecycleStateLocked() {
+ if (getLifecycleStateLocked() != Lifecycle.State.RESUMED
+ && getLifecycleStateLocked() != Lifecycle.State.STARTED) {
+ return;
+ }
+
+ // If anything is on top of the dream, we should stop touch handling.
+ boolean shouldPause = mShadeExpanded || mCommunalVisible || mBouncerShowing;
+
+ setLifecycleStateLocked(
+ shouldPause ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED);
+ }
+
@Override
public void onWakeUp() {
if (mDreamOverlayContainerViewController != null) {
@@ -292,6 +408,28 @@
}
}
+ @Override
+ public void onComeToFront() {
+ // Make sure the bouncer is closed. Expanding the shade effectively contracts the bouncer
+ // an equal amount.
+ if (mDreamOverlayContainerViewController != null
+ && mDreamOverlayContainerViewController.isBouncerShowing()) {
+ mScrimManager.getCurrentController().expand(
+ new ShadeExpansionChangeEvent(
+ /* fraction= */ 1.f,
+ /* expanded= */ false,
+ /* tracking= */ true));
+ }
+
+ // closeSystemDialogs takes care of closing anything that responds to the
+ // {@link Intent.ACTION_CLOSE_SYSTEM_DIALOGS} broadcast (which includes the notification
+ // shade).
+ mSystemDialogsCloser.closeSystemDialogs();
+
+ // Hide glanceable hub (this is a nop if glanceable hub is not open).
+ mCommunalInteractor.changeScene(CommunalScenes.Blank, null);
+ }
+
/**
* Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be
* called from the main executing thread. The window attributes closely mirror those that are
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java b/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java
new file mode 100644
index 0000000..6e7239a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams;
+
+/** Defines an interface for a class that is responsible for closing system dialogs. */
+public interface SystemDialogsCloser {
+ /** Close any open system dialogs. */
+ void closeSystemDialogs();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 31710ac..516b8c5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -31,6 +31,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
import com.android.systemui.dreams.DreamOverlayService;
+import com.android.systemui.dreams.SystemDialogsCloser;
import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
import com.android.systemui.dreams.homecontrols.DreamActivityProvider;
import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl;
@@ -146,6 +147,15 @@
return Optional.empty();
}
+ /**
+ * Provides an implementation for {@link SystemDialogsCloser} that calls
+ * {@link Context.closeSystemDialogs}.
+ */
+ @Provides
+ static SystemDialogsCloser providesSystemDialogsCloser(Context context) {
+ return () -> context.closeSystemDialogs();
+ }
+
/** */
@Provides
@Named(DREAM_ONLY_ENABLED_FOR_DOCK_USER)
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 1c047dd..fff0c58 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -98,7 +98,7 @@
// 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.
session.registerInputListener(ev -> {
- surfaces.handleDreamTouch((MotionEvent) ev);
+ surfaces.handleExternalShadeWindowTouch((MotionEvent) ev);
if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
var unused = session.pop();
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
index 304fdd6..ca6c8da 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt
@@ -23,11 +23,11 @@
object HapticSliderViewBinder {
/**
- * Binds a [SeekableSliderHapticPlugin] to a [View]. The binded view should be a
+ * Binds a [SeekbarHapticPlugin] to a [View]. The binded view should be a
* [android.widget.SeekBar] or a container of a [android.widget.SeekBar]
*/
@JvmStatic
- fun bind(view: View?, plugin: SeekableSliderHapticPlugin) {
+ fun bind(view: View?, plugin: SeekbarHapticPlugin) {
view?.repeatWhenAttached {
plugin.startInScope(lifecycleScope)
try {
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt
deleted file mode 100644
index cfa5294..0000000
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.haptics.slider
-
-import android.widget.SeekBar
-import android.widget.SeekBar.OnSeekBarChangeListener
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-
-/** An event producer for a Seekable element such as the Android [SeekBar] */
-class SeekableSliderEventProducer : SliderEventProducer, OnSeekBarChangeListener {
-
- /** The current event reported by a SeekBar */
- private val _currentEvent = MutableStateFlow(SliderEvent(SliderEventType.NOTHING, 0f))
-
- override fun produceEvents(): Flow<SliderEvent> = _currentEvent.asStateFlow()
-
- override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
- val eventType =
- if (fromUser) SliderEventType.PROGRESS_CHANGE_BY_USER
- else SliderEventType.PROGRESS_CHANGE_BY_PROGRAM
-
- _currentEvent.value = SliderEvent(eventType, normalizeProgress(seekBar, progress))
- }
-
- /**
- * Normalize the integer progress of a SeekBar to the range from 0F to 1F.
- *
- * @param[seekBar] The SeekBar that reports a progress.
- * @param[progress] The integer progress of the SeekBar within its min and max values.
- * @return The progress in the range from 0F to 1F.
- */
- private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float {
- if (seekBar.max == seekBar.min) {
- return 1.0f
- }
- val range = seekBar.max - seekBar.min
- return (progress - seekBar.min) / range.toFloat()
- }
-
- override fun onStartTrackingTouch(seekBar: SeekBar) {
- _currentEvent.update { previousEvent ->
- SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, previousEvent.currentProgress)
- }
- }
-
- override fun onStopTrackingTouch(seekBar: SeekBar) {
- _currentEvent.update { previousEvent ->
- SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, previousEvent.currentProgress)
- }
- }
-
- /** The arrow navigation that was operating the slider has stopped. */
- fun onArrowUp() {
- _currentEvent.update { previousEvent ->
- SliderEvent(SliderEventType.ARROW_UP, previousEvent.currentProgress)
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
rename to packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt
index ed82278..2007db34 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt
@@ -30,12 +30,12 @@
/**
* A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback.
*
- * A [SeekableSliderEventProducer] is used as the producer of slider events, a
+ * A [SliderStateProducer] is used as the producer of slider events, a
* [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback
- * depending on the state, and a [SeekableSliderTracker] is used as the state machine handler that
+ * depending on the state, and a [SliderStateTracker] is used as the state machine handler that
* tracks and manipulates the slider state.
*/
-class SeekableSliderHapticPlugin
+class SeekbarHapticPlugin
@JvmOverloads
constructor(
vibratorHelper: VibratorHelper,
@@ -46,7 +46,7 @@
private val velocityTracker = VelocityTracker.obtain()
- private val sliderEventProducer = SeekableSliderEventProducer()
+ private val sliderEventProducer = SliderStateProducer()
private val sliderHapticFeedbackProvider =
SliderHapticFeedbackProvider(
@@ -56,7 +56,7 @@
systemClock,
)
- private var sliderTracker: SeekableSliderTracker? = null
+ private var sliderTracker: SliderStateTracker? = null
private var pluginScope: CoroutineScope? = null
@@ -86,7 +86,7 @@
fun startInScope(scope: CoroutineScope) {
if (sliderTracker != null) stop()
sliderTracker =
- SeekableSliderTracker(
+ SliderStateTracker(
sliderHapticFeedbackProvider,
sliderEventProducer,
scope,
@@ -116,28 +116,52 @@
/** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */
fun onStartTrackingTouch(seekBar: SeekBar) {
if (isTracking) {
- sliderEventProducer.onStartTrackingTouch(seekBar)
+ sliderEventProducer.onStartTracking(true)
}
}
/** onProgressChanged event from the slider's [android.widget.SeekBar] */
fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (isTracking) {
- sliderEventProducer.onProgressChanged(seekBar, progress, fromUser)
+ if (sliderTracker?.currentState == SliderState.IDLE && !fromUser) {
+ // This case translates to the slider starting to track program changes
+ sliderEventProducer.resetWithProgress(normalizeProgress(seekBar, progress))
+ sliderEventProducer.onStartTracking(false)
+ } else {
+ sliderEventProducer.onProgressChanged(
+ fromUser,
+ normalizeProgress(seekBar, progress),
+ )
+ }
}
}
+ /**
+ * Normalize the integer progress of a SeekBar to the range from 0F to 1F.
+ *
+ * @param[seekBar] The SeekBar that reports a progress.
+ * @param[progress] The integer progress of the SeekBar within its min and max values.
+ * @return The progress in the range from 0F to 1F.
+ */
+ private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float {
+ if (seekBar.max == seekBar.min) {
+ return 1.0f
+ }
+ val range = seekBar.max - seekBar.min
+ return (progress - seekBar.min) / range.toFloat()
+ }
+
/** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */
fun onStopTrackingTouch(seekBar: SeekBar) {
if (isTracking) {
- sliderEventProducer.onStopTrackingTouch(seekBar)
+ sliderEventProducer.onStopTracking(true)
}
}
- /** onArrowUp event recorded */
- fun onArrowUp() {
+ /** Programmatic changes have stopped */
+ private fun onStoppedTrackingProgram() {
if (isTracking) {
- sliderEventProducer.onArrowUp()
+ sliderEventProducer.onStopTracking(false)
}
}
@@ -146,7 +170,7 @@
*
* This event is used to estimate the key-up event based on a running a timer as a waiting
* coroutine in the [pluginScope]. A key-up event in a slider corresponds to an onArrowUp event.
- * Therefore, [onArrowUp] must be called after the timeout.
+ * Therefore, [onStoppedTrackingProgram] must be called after the timeout.
*/
fun onKeyDown() {
if (!isTracking) return
@@ -158,7 +182,7 @@
keyUpJob =
pluginScope?.launch {
delay(KEY_UP_TIMEOUT)
- onArrowUp()
+ onStoppedTrackingProgram()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt
index 4a63941..0edef99 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt
@@ -22,6 +22,8 @@
NOTHING,
/* The slider has captured a touch input and is tracking touch events. */
STARTED_TRACKING_TOUCH,
+ /* The slider started tracking programmatic value changes */
+ STARTED_TRACKING_PROGRAM,
/* The slider progress is changing due to user touch input. */
PROGRESS_CHANGE_BY_USER,
/* The slider progress is changing programmatically. */
@@ -29,5 +31,5 @@
/* The slider has stopped tracking touch events. */
STOPPED_TRACKING_TOUCH,
/* The external (not touch) stimulus that was modifying the slider progress has stopped. */
- ARROW_UP,
+ STOPPED_TRACKING_PROGRAM,
}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt
new file mode 100644
index 0000000..1124ab1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.slider
+
+import androidx.annotation.FloatRange
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+/** A stateful producer of [SliderEvent] */
+class SliderStateProducer : SliderEventProducer {
+
+ /** The current event of a slider */
+ private val _currentEvent = MutableStateFlow(SliderEvent(SliderEventType.NOTHING, 0f))
+
+ override fun produceEvents(): Flow<SliderEvent> = _currentEvent.asStateFlow()
+
+ fun onProgressChanged(fromUser: Boolean, @FloatRange(from = 0.0, to = 1.0) progress: Float) {
+ val eventType =
+ if (fromUser) SliderEventType.PROGRESS_CHANGE_BY_USER
+ else SliderEventType.PROGRESS_CHANGE_BY_PROGRAM
+
+ _currentEvent.value = SliderEvent(eventType, progress)
+ }
+
+ fun onStartTracking(fromUser: Boolean) {
+ val eventType =
+ if (fromUser) SliderEventType.STARTED_TRACKING_TOUCH
+ else SliderEventType.STARTED_TRACKING_PROGRAM
+ _currentEvent.update { previousEvent ->
+ SliderEvent(eventType, previousEvent.currentProgress)
+ }
+ }
+
+ fun onStopTracking(fromUser: Boolean) {
+ val eventType =
+ if (fromUser) SliderEventType.STOPPED_TRACKING_TOUCH
+ else SliderEventType.STOPPED_TRACKING_PROGRAM
+ _currentEvent.update { previousEvent ->
+ SliderEvent(eventType, previousEvent.currentProgress)
+ }
+ }
+
+ fun resetWithProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
+ _currentEvent.value = SliderEvent(SliderEventType.NOTHING, progress)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
rename to packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt
index 0af3038..14cf411 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt
@@ -25,11 +25,12 @@
import kotlinx.coroutines.launch
/**
- * Slider tracker attached to a seekable slider.
+ * Slider tracker attached to a slider.
*
- * The tracker runs a state machine to execute actions on touch-based events typical of a seekable
- * slider such as [android.widget.SeekBar]. Coroutines responsible for running the state machine,
- * collecting slider events and maintaining waiting states are run on the provided [CoroutineScope].
+ * The tracker runs a state machine to execute actions on touch-based events typical of a general
+ * slider (including a [android.widget.SeekBar]). Coroutines responsible for running the state
+ * machine, collecting slider events and maintaining waiting states are run on the provided
+ * [CoroutineScope].
*
* @param[sliderStateListener] Listener of the slider state.
* @param[sliderEventProducer] Producer of slider events arising from the slider.
@@ -37,7 +38,7 @@
* events and the launch of timer jobs.
* @property[config] Configuration parameters of the slider tracker.
*/
-class SeekableSliderTracker(
+class SliderStateTracker(
sliderStateListener: SliderStateListener,
sliderEventProducer: SliderEventProducer,
trackerScope: CoroutineScope,
@@ -79,7 +80,7 @@
// This will disambiguate between an imprecise touch that acquires the slider handle,
// and a select and jump operation in the slider track.
setState(SliderState.WAIT)
- } else if (newEventType == SliderEventType.PROGRESS_CHANGE_BY_PROGRAM) {
+ } else if (newEventType == SliderEventType.STARTED_TRACKING_PROGRAM) {
val state =
if (bookendReached(currentProgress)) SliderState.ARROW_HANDLE_REACHED_BOOKEND
else SliderState.ARROW_HANDLE_MOVED_ONCE
@@ -227,7 +228,7 @@
}
SliderEventType.PROGRESS_CHANGE_BY_PROGRAM ->
SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY
- SliderEventType.ARROW_UP -> SliderState.IDLE
+ SliderEventType.STOPPED_TRACKING_PROGRAM -> SliderState.IDLE
else -> SliderState.ARROW_HANDLE_MOVED_ONCE
}
setState(nextState)
@@ -237,7 +238,7 @@
val reachedBookend = bookendReached(currentProgress)
val nextState =
when (newEventType) {
- SliderEventType.ARROW_UP -> SliderState.IDLE
+ SliderEventType.STOPPED_TRACKING_PROGRAM -> SliderState.IDLE
SliderEventType.STARTED_TRACKING_TOUCH -> {
// Launching the timer and going to WAIT
timerJob = launchTimer()
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
new file mode 100644
index 0000000..692fbb0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut
+
+import android.graphics.Insets
+import android.os.Bundle
+import android.view.View
+import android.view.WindowInsets
+import androidx.activity.BackEventCompat
+import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedCallback
+import androidx.core.view.updatePadding
+import com.android.systemui.res.R
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+
+/**
+ * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
+ * small and large screen devices.
+ */
+class ShortcutHelperActivity : ComponentActivity() {
+
+ private val bottomSheetContainer
+ get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
+
+ private val bottomSheet
+ get() = requireViewById<View>(R.id.shortcut_helper_sheet)
+
+ private val bottomSheetBehavior
+ get() = BottomSheetBehavior.from(bottomSheet)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setupEdgeToEdge()
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_keyboard_shortcut_helper)
+ setUpBottomSheetWidth()
+ setUpInsets()
+ setUpPredictiveBack()
+ setUpSheetDismissListener()
+ setUpDismissOnTouchOutside()
+ }
+
+ private fun setupEdgeToEdge() {
+ // Draw behind system bars
+ window.setDecorFitsSystemWindows(false)
+ }
+
+ private fun setUpBottomSheetWidth() {
+ val sheetScreenWidthFraction =
+ resources.getFloat(R.dimen.shortcut_helper_screen_width_fraction)
+ // maxWidth needs to be set before the sheet is drawn, otherwise the call will have no
+ // effect.
+ val screenWidth = resources.displayMetrics.widthPixels
+ bottomSheetBehavior.maxWidth = (sheetScreenWidthFraction * screenWidth).toInt()
+ }
+
+ private fun setUpInsets() {
+ bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
+ val safeDrawingInsets = insets.safeDrawing
+ // Make sure the bottom sheet is not covered by the status bar.
+ bottomSheetContainer.updatePadding(top = safeDrawingInsets.top)
+ // Make sure the contents inside of the bottom sheet are not hidden by system bars, or
+ // cutouts.
+ bottomSheet.updatePadding(
+ left = safeDrawingInsets.left,
+ right = safeDrawingInsets.right,
+ bottom = safeDrawingInsets.bottom
+ )
+ // The bottom sheet has to be expanded only after setting up insets, otherwise there is
+ // a bug and it will not use full height.
+ expandBottomSheet()
+
+ // Return CONSUMED if you don't want want the window insets to keep passing
+ // down to descendant views.
+ WindowInsets.CONSUMED
+ }
+ }
+
+ private fun expandBottomSheet() {
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+ bottomSheetBehavior.skipCollapsed = true
+ }
+
+ private fun setUpPredictiveBack() {
+ val onBackPressedCallback =
+ object : OnBackPressedCallback(/* enabled= */ true) {
+ override fun handleOnBackStarted(backEvent: BackEventCompat) {
+ bottomSheetBehavior.startBackProgress(backEvent)
+ }
+
+ override fun handleOnBackProgressed(backEvent: BackEventCompat) {
+ bottomSheetBehavior.updateBackProgress(backEvent)
+ }
+
+ override fun handleOnBackPressed() {
+ bottomSheetBehavior.handleBackInvoked()
+ }
+
+ override fun handleOnBackCancelled() {
+ bottomSheetBehavior.cancelBackProgress()
+ }
+ }
+ onBackPressedDispatcher.addCallback(
+ owner = this,
+ onBackPressedCallback = onBackPressedCallback
+ )
+ }
+
+ private fun setUpSheetDismissListener() {
+ bottomSheetBehavior.addBottomSheetCallback(
+ object : BottomSheetCallback() {
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ if (newState == STATE_HIDDEN) {
+ finish()
+ }
+ }
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {}
+ }
+ )
+ }
+
+ private fun setUpDismissOnTouchOutside() {
+ bottomSheetContainer.setOnClickListener { finish() }
+ }
+}
+
+private val WindowInsets.safeDrawing
+ get() =
+ getInsets(WindowInsets.Type.systemBars())
+ .union(getInsets(WindowInsets.Type.ime()))
+ .union(getInsets(WindowInsets.Type.displayCutout()))
+
+private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 7224536..d191768 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -226,7 +226,7 @@
val ambientIndicationVisible: Flow<Boolean> = repository.ambientIndicationVisible.asStateFlow()
/** Whether the primary bouncer is showing or not. */
- val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow
+ @JvmField val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow
/** Whether the alternate bouncer is showing or not. */
val alternateBouncerShowing: Flow<Boolean> = bouncerRepository.alternateBouncerVisible
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index 92612b8..7d05539 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -87,6 +87,7 @@
}
/** Whether either of the bouncers are visible when we're FINISHED in the given state. */
+ @JvmStatic
fun isBouncerState(state: KeyguardState): Boolean {
return state == PRIMARY_BOUNCER || state == ALTERNATE_BOUNCER
}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index 5dafd94..c997617 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -17,6 +17,7 @@
package com.android.systemui.lifecycle
+import android.os.Trace
import android.view.View
import android.view.ViewTreeObserver
import androidx.annotation.MainThread
@@ -73,7 +74,7 @@
Dispatchers.Main + createCoroutineTracingContext() + coroutineContext
val traceName =
if (Compile.IS_DEBUG && coroutineTracing()) {
- traceSectionName()
+ inferTraceSectionName()
} else {
DEFAULT_TRACE_NAME
}
@@ -197,16 +198,21 @@
frame.className != CURRENT_CLASS_NAME && frame.className != JAVA_ADAPTER_CLASS_NAME
/** Get a name for the trace section include the name of the call site. */
-private fun traceSectionName(): String {
- val interestingFrame =
- StackWalker.getInstance().walk { stream ->
- stream.filter(::isFrameInteresting).limit(5).findFirst()
+private fun inferTraceSectionName(): String {
+ try {
+ Trace.traceBegin(Trace.TRACE_TAG_APP, "RepeatWhenAttachedKt#inferTraceSectionName")
+ val interestingFrame =
+ StackWalker.getInstance().walk { stream ->
+ stream.filter(::isFrameInteresting).limit(5).findFirst()
+ }
+ if (interestingFrame.isPresent) {
+ val f = interestingFrame.get()
+ return "${f.className}#${f.methodName}:${f.lineNumber} [$DEFAULT_TRACE_NAME]"
+ } else {
+ return DEFAULT_TRACE_NAME
}
- if (interestingFrame.isPresent) {
- val frame = interestingFrame.get()
- return "${frame.className}#${frame.methodName}:${frame.lineNumber} [$DEFAULT_TRACE_NAME]"
- } else {
- return DEFAULT_TRACE_NAME
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_APP)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 9790a52..d5b05ef 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -1348,15 +1348,12 @@
mDefaultDataSubId = defaultDataSubId;
}
- boolean mayLaunchShareWifiSettings(WifiEntry wifiEntry) {
+ boolean mayLaunchShareWifiSettings(WifiEntry wifiEntry, View view) {
Intent intent = getConfiguratorQrCodeGeneratorIntentOrNull(wifiEntry);
if (intent == null) {
return false;
}
- if (mCallback != null) {
- mCallback.dismissDialog();
- }
- mActivityStarter.startActivity(intent, false /* dismissShade */);
+ startActivity(intent, view);
return true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
index 24089a7..1a881b6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
@@ -395,7 +395,7 @@
});
mDoneButton.setOnClickListener(v -> dialog.dismiss());
mShareWifiButton.setOnClickListener(v -> {
- if (mInternetDialogController.mayLaunchShareWifiSettings(mConnectedWifiEntry)) {
+ if (mInternetDialogController.mayLaunchShareWifiSettings(mConnectedWifiEntry, v)) {
mUiEventLogger.log(InternetDialogEvent.SHARE_WIFI_QS_BUTTON_CLICKED);
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index b425fb9..083cee7 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -33,7 +33,7 @@
import com.android.systemui.Gefingerpoken;
import com.android.systemui.classifier.Classifier;
import com.android.systemui.haptics.slider.HapticSliderViewBinder;
-import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
+import com.android.systemui.haptics.slider.SeekbarHapticPlugin;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.res.R;
@@ -65,7 +65,7 @@
private final FalsingManager mFalsingManager;
private final UiEventLogger mUiEventLogger;
- private final SeekableSliderHapticPlugin mBrightnessSliderHapticPlugin;
+ private final SeekbarHapticPlugin mBrightnessSliderHapticPlugin;
private final ActivityStarter mActivityStarter;
private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() {
@@ -89,7 +89,7 @@
BrightnessSliderView brightnessSliderView,
FalsingManager falsingManager,
UiEventLogger uiEventLogger,
- SeekableSliderHapticPlugin brightnessSliderHapticPlugin,
+ SeekbarHapticPlugin brightnessSliderHapticPlugin,
ActivityStarter activityStarter) {
super(brightnessSliderView);
mFalsingManager = falsingManager;
@@ -314,7 +314,7 @@
int layout = getLayout();
BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context)
.inflate(layout, viewRoot, false);
- SeekableSliderHapticPlugin plugin = new SeekableSliderHapticPlugin(
+ SeekbarHapticPlugin plugin = new SeekbarHapticPlugin(
mVibratorHelper,
mSystemClock);
if (hapticBrightnessSlider()) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index a8481cd..a5a5474 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -28,10 +28,14 @@
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.theme.PlatformTheme
import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.ambient.touch.TouchMonitor
+import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
import com.android.systemui.communal.dagger.Communal
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.ui.compose.CommunalContainer
@@ -45,6 +49,8 @@
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.BooleanFlowOperators.or
import com.android.systemui.util.kotlin.collectFlow
import javax.inject.Inject
@@ -67,12 +73,27 @@
private val shadeInteractor: ShadeInteractor,
private val powerManager: PowerManager,
private val communalColors: CommunalColors,
- @Communal private val dataSourceDelegator: SceneDataSourceDelegator,
-) {
+ private val ambientTouchComponentFactory: AmbientTouchComponent.Factory,
+ @Communal private val dataSourceDelegator: SceneDataSourceDelegator
+) : LifecycleOwner {
/** The container view for the hub. This will not be initialized until [initView] is called. */
private var communalContainerView: View? = null
/**
+ * This lifecycle is used to control when the [touchMonitor] listens to touches. The lifecycle
+ * should only be [Lifecycle.State.RESUMED] when the hub is showing and not covered by anything,
+ * such as the notification shade or bouncer.
+ */
+ private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
+
+ /**
+ * This [TouchMonitor] listens for top and bottom swipe gestures globally when the hub is open.
+ * When a top or bottom swipe is detected, they will be intercepted and used to open the
+ * notification shade/bouncer.
+ */
+ private var touchMonitor: TouchMonitor? = null
+
+ /**
* The width of the area in which a right edge swipe can open the hub, in pixels. Read from
* resources when [initView] is called.
*/
@@ -80,20 +101,6 @@
private var rightEdgeSwipeRegionWidth: Int = 0
/**
- * The height of the area in which a top edge swipe while the hub is open will not intercept
- * touches, in pixels. This allows the top edge swipe to instead open the notification shade.
- * Read from resources when [initView] is called.
- */
- private var topEdgeSwipeRegionWidth: Int = 0
-
- /**
- * The height of the area in which a bottom edge swipe while the hub is open will not intercept
- * touches, in pixels. This allows the bottom edge swipe to instead open the bouncer. Read from
- * resources when [initView] is called.
- */
- private var bottomEdgeSwipeRegionWidth: Int = 0
-
- /**
* True if we are currently tracking a gesture for opening the hub that started in the edge
* swipe region.
*/
@@ -102,9 +109,6 @@
/** True if we are currently tracking a touch on the hub while it's open. */
private var isTrackingHubTouch = false
- /** True if we are tracking a top or bottom swipe gesture while the hub is open. */
- private var isTrackingHubGesture = false
-
/**
* True if the hub UI is fully open, meaning it should receive touch input.
*
@@ -121,9 +125,15 @@
private var anyBouncerShowing = false
/**
- * True if the shade is fully expanded, meaning the hub should not receive any touch input.
+ * True if the shade is fully expanded and the user is not interacting with it anymore, meaning
+ * the hub should not receive any touch input.
*
- * Tracks [ShadeInteractor.isAnyFullyExpanded].
+ * We need to not pause the touch handling lifecycle as soon as the shade opens because if the
+ * user swipes down, then back up without lifting their finger, the lifecycle will be paused
+ * then resumed, and resuming force-stops all active touch sessions. This means the shade will
+ * not receive the end of the gesture and will be stuck open.
+ *
+ * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting].
*/
private var shadeShowing = false
@@ -132,8 +142,6 @@
* and just let the dream overlay's touch handling deal with them.
*
* Tracks [KeyguardInteractor.isDreaming].
- *
- * TODO(b/328838259): figure out a proper solution for touch handling above the lock screen too
*/
private var isDreaming = false
@@ -192,28 +200,45 @@
throw RuntimeException("Communal view has already been initialized")
}
+ if (touchMonitor == null) {
+ touchMonitor =
+ ambientTouchComponentFactory.create(this, HashSet()).getTouchMonitor().apply {
+ init()
+ }
+ }
+ lifecycleRegistry.currentState = Lifecycle.State.CREATED
+
communalContainerView = containerView
rightEdgeSwipeRegionWidth =
containerView.resources.getDimensionPixelSize(
R.dimen.communal_right_edge_swipe_region_width
)
- topEdgeSwipeRegionWidth =
- containerView.resources.getDimensionPixelSize(
- R.dimen.communal_top_edge_swipe_region_height
- )
- bottomEdgeSwipeRegionWidth =
- containerView.resources.getDimensionPixelSize(
- R.dimen.communal_bottom_edge_swipe_region_height
- )
collectFlow(
containerView,
keyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState),
- { anyBouncerShowing = it }
+ {
+ anyBouncerShowing = it
+ updateLifecycleState()
+ }
)
- collectFlow(containerView, communalInteractor.isCommunalShowing, { hubShowing = it })
- collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it })
+ collectFlow(
+ containerView,
+ communalInteractor.isCommunalShowing,
+ {
+ hubShowing = it
+ updateLifecycleState()
+ }
+ )
+ collectFlow(
+ containerView,
+ and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
+ {
+ shadeShowing = it
+ updateLifecycleState()
+ }
+ )
collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
communalContainerView = containerView
@@ -221,10 +246,24 @@
return containerView
}
+ /**
+ * Updates the lifecycle stored by the [lifecycleRegistry] to control when the [touchMonitor]
+ * should listen for and intercept top and bottom swipes.
+ */
+ private fun updateLifecycleState() {
+ val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing)
+ if (shouldInterceptGestures) {
+ lifecycleRegistry.currentState = Lifecycle.State.RESUMED
+ } else {
+ lifecycleRegistry.currentState = Lifecycle.State.STARTED
+ }
+ }
+
/** Removes the container view from its parent. */
fun disposeView() {
communalContainerView?.let {
(it.parent as ViewGroup).removeView(it)
+ lifecycleRegistry.currentState = Lifecycle.State.CREATED
communalContainerView = null
}
}
@@ -262,15 +301,7 @@
if (isDown && !hubOccluded) {
// Only intercept down events if the hub isn't occluded by the bouncer or
// notification shade.
- val y = ev.rawY
- val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth
- val bottomSwipe = y >= view.height - bottomEdgeSwipeRegionWidth
-
- if (topSwipe || bottomSwipe) {
- isTrackingHubGesture = true
- } else {
- isTrackingHubTouch = true
- }
+ isTrackingHubTouch = true
}
if (isTrackingHubTouch) {
@@ -283,19 +314,6 @@
// gesture
// may return false from dispatchTouchEvent.
return true
- } else if (isTrackingHubGesture) {
- // Tracking a top or bottom swipe on the hub UI.
- if (isUp || isCancel) {
- isTrackingHubGesture = false
- }
-
- // If we're dreaming, intercept touches so the hub UI doesn't receive them, but
- // don't do anything so that the dream's touch handling takes care of opening
- // the bouncer or shade.
- //
- // If we're not dreaming, we don't intercept touches at the top/bottom edge so that
- // swipes can open the notification shade and bouncer.
- return isDreaming
}
return false
@@ -347,4 +365,7 @@
0
)
}
+
+ override val lifecycle: Lifecycle
+ get() = lifecycleRegistry
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index adcb14a..8b7e11c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -448,6 +448,9 @@
|| mScreenOffAnimationController.shouldIgnoreKeyguardTouches()) {
mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ } else if (state.glanceableHubShowing) {
+ mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
+ mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
} else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) {
mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
// Make sure to remove FLAG_ALT_FOCUSABLE_IM when keyguard needs input.
@@ -611,6 +614,7 @@
state.panelVisible,
state.shadeOrQsExpanded,
state.notificationShadeFocusable,
+ state.glanceableHubShowing,
state.bouncerShowing,
state.keyguardFadingAway,
state.keyguardGoingAway,
@@ -740,6 +744,12 @@
}
@Override
+ public void setGlanceableHubShowing(boolean showing) {
+ mCurrentState.glanceableHubShowing = showing;
+ apply(mCurrentState);
+ }
+
+ @Override
public void setBackdropShowing(boolean showing) {
mCurrentState.mediaBackdropShowing = showing;
apply(mCurrentState);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
index e0a98b3..6a4b52a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -35,6 +35,7 @@
@JvmField var shadeOrQsExpanded: Boolean = false,
@JvmField var notificationShadeFocusable: Boolean = false,
@JvmField var bouncerShowing: Boolean = false,
+ @JvmField var glanceableHubShowing: Boolean = false,
@JvmField var keyguardFadingAway: Boolean = false,
@JvmField var keyguardGoingAway: Boolean = false,
@JvmField var qsExpanded: Boolean = false,
@@ -79,6 +80,7 @@
shadeOrQsExpanded.toString(),
notificationShadeFocusable.toString(),
bouncerShowing.toString(),
+ glanceableHubShowing.toString(),
keyguardFadingAway.toString(),
keyguardGoingAway.toString(),
qsExpanded.toString(),
@@ -119,6 +121,7 @@
panelVisible: Boolean,
panelExpanded: Boolean,
notificationShadeFocusable: Boolean,
+ glanceableHubShowing: Boolean,
bouncerShowing: Boolean,
keyguardFadingAway: Boolean,
keyguardGoingAway: Boolean,
@@ -149,6 +152,7 @@
this.panelVisible = panelVisible
this.shadeOrQsExpanded = panelExpanded
this.notificationShadeFocusable = notificationShadeFocusable
+ this.glanceableHubShowing = glanceableHubShowing
this.bouncerShowing = bouncerShowing
this.keyguardFadingAway = keyguardFadingAway
this.keyguardGoingAway = keyguardGoingAway
@@ -197,6 +201,7 @@
"panelVisible",
"panelExpanded",
"notificationShadeFocusable",
+ "glanceableHubShowing",
"bouncerShowing",
"keyguardFadingAway",
"keyguardGoingAway",
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 907cf5e..44f86da 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -25,7 +25,6 @@
import android.app.StatusBarManager;
import android.util.Log;
import android.view.GestureDetector;
-import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
@@ -74,14 +73,14 @@
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.util.time.SystemClock;
+import kotlinx.coroutines.ExperimentalCoroutinesApi;
+
import java.io.PrintWriter;
import java.util.Optional;
import java.util.function.Consumer;
import javax.inject.Inject;
-import kotlinx.coroutines.ExperimentalCoroutinesApi;
-
/**
* Controller for {@link NotificationShadeWindowView}.
*/
@@ -137,6 +136,11 @@
private final PanelExpansionInteractor mPanelExpansionInteractor;
private final ShadeExpansionStateManager mShadeExpansionStateManager;
+ /**
+ * If {@code true}, an external touch sent in {@link #handleExternalTouch(MotionEvent)} has been
+ * intercepted and all future touch events for the gesture should be processed by this view.
+ */
+ private boolean mExternalTouchIntercepted = false;
private boolean mIsTrackingBarGesture = false;
private boolean mIsOcclusionTransitionRunning = false;
private DisableSubpixelTextTransitionListener mDisableSubpixelTextTransitionListener;
@@ -253,11 +257,28 @@
}
/**
- * Handle a touch event while dreaming by forwarding the event to the content view.
+ * Handle a touch event while dreaming or on the hub by forwarding the event to the content
+ * view.
+ * <p>
+ * Since important logic for handling touches lives in the dispatch/intercept phases, we
+ * simulate going through all of these stages before sending onTouchEvent if intercepted.
+ *
* @param event The event to forward.
*/
- public void handleDreamTouch(MotionEvent event) {
- mView.dispatchTouchEvent(event);
+ public void handleExternalTouch(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mExternalTouchIntercepted = false;
+ }
+
+ if (!mView.dispatchTouchEvent(event)) {
+ return;
+ }
+ if (!mExternalTouchIntercepted) {
+ mExternalTouchIntercepted = mView.onInterceptTouchEvent(event);
+ }
+ if (mExternalTouchIntercepted) {
+ mView.onTouchEvent(event);
+ }
}
/** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */
diff --git a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
index 8eed097..396c5f2 100644
--- a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt
@@ -16,7 +16,8 @@
package com.android.systemui.startable
import com.android.systemui.CoreStartable
-import kotlin.reflect.KClass
+import java.lang.annotation.Documented
+import javax.inject.Qualifier
/**
* Allows a [CoreStartable] to declare that it must be started after its dependencies.
@@ -24,7 +25,4 @@
* This creates a partial, topological ordering. See [com.android.systemui.SystemUIApplication] for
* how this ordering is enforced at runtime.
*/
-@MustBeDocumented
-@Target(AnnotationTarget.CLASS)
-@Retention(AnnotationRetention.RUNTIME)
-annotation class Dependencies(vararg val value: KClass<*> = [])
+@Qualifier @Documented @Retention(AnnotationRetention.RUNTIME) annotation class Dependencies()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index e669556..707d59aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -86,6 +86,9 @@
/** Sets the state of whether the bouncer is showing or not. */
default void setBouncerShowing(boolean showing) {}
+ /** Sets the state of whether the glanceable hub is showing or not. */
+ default void setGlanceableHubShowing(boolean showing) {}
+
/** Sets the state of whether the backdrop is showing or not. */
default void setBackdropShowing(boolean showing) {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
index d2fe20d..8104755 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java
@@ -23,7 +23,6 @@
import com.android.systemui.CoreStartable;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.startable.Dependencies;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import java.lang.annotation.Retention;
@@ -31,7 +30,6 @@
/**
* Sends updates to {@link StateListener}s about changes to the status bar state and dozing state
*/
-@Dependencies(CentralSurfaces.class)
public interface SysuiStatusBarStateController extends StatusBarStateController, CoreStartable {
// TODO: b/115739177 (remove this explicit ordering if we can)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 2651d2e..7d97428 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -37,6 +37,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
+import com.android.systemui.CoreStartable;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.RemoteAnimationRunnerCompat;
@@ -50,7 +51,7 @@
import java.io.PrintWriter;
/** */
-public interface CentralSurfaces extends Dumpable, LifecycleOwner {
+public interface CentralSurfaces extends Dumpable, LifecycleOwner, CoreStartable {
boolean MULTIUSER_DEBUG = false;
// Should match the values in PhoneWindowManager
String SYSTEM_DIALOG_REASON_KEY = "reason";
@@ -182,6 +183,9 @@
return contextForUser.getPackageManager();
}
+ /** Default impl for CoreStartable. */
+ default void start() {}
+
boolean updateIsKeyguard();
boolean updateIsKeyguard(boolean forceStateChange);
@@ -279,11 +283,12 @@
void awakenDreams();
/**
- * Handle a touch event while dreaming when the touch was initiated within a prescribed
- * swipeable area. This method is provided for cases where swiping in certain areas of a dream
- * should be handled by CentralSurfaces instead (e.g. swiping communal hub open).
+ * Handle a touch event while dreaming or on the glanceable hub when the touch was initiated
+ * within a prescribed swipeable area. This method is provided for cases where swiping in
+ * certain areas should be handled by CentralSurfaces instead (e.g. swiping hub open, opening
+ * the notification shade over dream or hub).
*/
- void handleDreamTouch(MotionEvent event);
+ void handleExternalShadeWindowTouch(MotionEvent event);
boolean isBouncerShowing();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
index 8af7ee8..d5e66ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
@@ -79,7 +79,7 @@
override fun updateScrimController() {}
override fun shouldIgnoreTouch() = false
override fun isDeviceInteractive() = false
- override fun handleDreamTouch(event: MotionEvent?) {}
+ override fun handleExternalShadeWindowTouch(event: MotionEvent?) {}
override fun awakenDreams() {}
override fun isBouncerShowing() = false
override fun isBouncerShowingScrimmed() = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index e9aa7aa..b2b2cea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2928,8 +2928,8 @@
};
@Override
- public void handleDreamTouch(MotionEvent event) {
- getNotificationShadeWindowViewController().handleDreamTouch(event);
+ public void handleExternalShadeWindowTouch(MotionEvent event) {
+ getNotificationShadeWindowViewController().handleExternalTouch(event);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 4129b3a..b80ff38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -42,6 +42,8 @@
import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl
import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModelImpl
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
@@ -85,6 +87,11 @@
impl: DeviceBasedSatelliteRepositoryImpl
): DeviceBasedSatelliteRepository
+ @Binds
+ abstract fun deviceBasedSatelliteViewModel(
+ impl: DeviceBasedSatelliteViewModelImpl
+ ): DeviceBasedSatelliteViewModel
+
@Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository
@Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index a0291b8..f2255f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -16,13 +16,16 @@
package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
+import android.content.Context
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
+import com.android.systemui.res.R
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog
import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@@ -42,15 +45,30 @@
* View-Model for the device-based satellite icon. This icon will only show in the status bar if
* satellite is available AND all other service states are considered OOS.
*/
+interface DeviceBasedSatelliteViewModel {
+ /**
+ * The satellite icon that should be displayed, or null if no satellite icon should be
+ * displayed.
+ */
+ val icon: StateFlow<Icon?>
+
+ /**
+ * The satellite-related text that should be used as the carrier text string when satellite is
+ * active, or null if the carrier text string shouldn't include any satellite information.
+ */
+ val carrierText: StateFlow<String?>
+}
+
@OptIn(ExperimentalCoroutinesApi::class)
-class DeviceBasedSatelliteViewModel
+class DeviceBasedSatelliteViewModelImpl
@Inject
constructor(
+ context: Context,
interactor: DeviceBasedSatelliteInteractor,
@Application scope: CoroutineScope,
airplaneModeRepository: AirplaneModeRepository,
@OemSatelliteInputLog logBuffer: LogBuffer,
-) {
+) : DeviceBasedSatelliteViewModel {
private val shouldShowIcon: Flow<Boolean> =
interactor.areAllConnectionsOutOfService.flatMapLatest { allOos ->
if (!allOos) {
@@ -87,7 +105,7 @@
}
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
- val icon: StateFlow<Icon?> =
+ override val icon: StateFlow<Icon?> =
combine(
shouldActuallyShowIcon,
interactor.connectionState,
@@ -101,6 +119,26 @@
}
.stateIn(scope, SharingStarted.WhileSubscribed(), null)
+ override val carrierText: StateFlow<String?> =
+ combine(
+ shouldActuallyShowIcon,
+ interactor.connectionState,
+ ) { shouldShow, connectionState ->
+ if (shouldShow) {
+ when (connectionState) {
+ SatelliteConnectionState.Connected ->
+ context.getString(R.string.satellite_connected_carrier_text)
+ SatelliteConnectionState.On ->
+ context.getString(R.string.satellite_not_connected_carrier_text)
+ SatelliteConnectionState.Off,
+ SatelliteConnectionState.Unknown -> null
+ }
+ } else {
+ null
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
companion object {
private const val TAG = "DeviceBasedSatelliteViewModel"
private val DELAY_DURATION = 10.seconds
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 2245541..cd82e1d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -118,7 +118,7 @@
import com.android.systemui.Prefs;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.haptics.slider.HapticSliderViewBinder;
-import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
+import com.android.systemui.haptics.slider.SeekbarHapticPlugin;
import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig;
import com.android.systemui.media.dialog.MediaOutputDialogManager;
import com.android.systemui.plugins.VolumeDialog;
@@ -2640,7 +2640,7 @@
private ObjectAnimator anim; // slider progress animation for non-touch-related updates
private int animTargetProgress;
private int lastAudibleLevel = 1;
- private SeekableSliderHapticPlugin mHapticPlugin;
+ private SeekbarHapticPlugin mHapticPlugin;
private int mProgressHapticsType = PROGRESS_HAPTICS_DISABLED;
void setIcon(int iconRes, Resources.Theme theme) {
@@ -2658,7 +2658,7 @@
com.android.systemui.util.time.SystemClock systemClock) {
if (mHapticPlugin != null) return;
- mHapticPlugin = new SeekableSliderHapticPlugin(
+ mHapticPlugin = new SeekbarHapticPlugin(
vibratorHelper,
systemClock,
sSliderHapticFeedbackConfig);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
index 303ae97..11d31c6 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
@@ -58,12 +58,15 @@
import com.android.keyguard.logging.CarrierTextManagerLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.log.LogBufferHelperKt;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.FakeDeviceBasedSatelliteViewModel;
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository;
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel;
import com.android.systemui.telephony.TelephonyListenerManager;
import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
@@ -78,6 +81,8 @@
import java.util.HashMap;
import java.util.List;
+import kotlinx.coroutines.test.TestScope;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class CarrierTextManagerTest extends SysuiTestCase {
@@ -99,6 +104,8 @@
TEST_CARRIER, TEST_CARRIER, NAME_SOURCE_CARRIER_ID, 0xFFFFFF, "",
DATA_ROAMING_ENABLE, null, null, null, null, false, null, "");
private FakeWifiRepository mWifiRepository = new FakeWifiRepository();
+ private final FakeDeviceBasedSatelliteViewModel mSatelliteViewModel =
+ new FakeDeviceBasedSatelliteViewModel();
@Mock
private WakefulnessLifecycle mWakefulnessLifecycle;
@Mock
@@ -124,6 +131,10 @@
new CarrierTextManagerLogger(
LogBufferHelperKt.logcatLogBuffer("CarrierTextManagerLog"));
+ private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+ private final TestScope mTestScope = mKosmos.getTestScope();
+ private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
+
private Void checkMainThread(InvocationOnMock inv) {
assertThat(mMainExecutor.isExecuting()).isTrue();
assertThat(mBgExecutor.isExecuting()).isFalse();
@@ -156,9 +167,18 @@
when(mTelephonyManager.getActiveModemCount()).thenReturn(3);
mCarrierTextManager = new CarrierTextManager.Builder(
- mContext, mContext.getResources(), mWifiRepository,
- mTelephonyManager, mTelephonyListenerManager, mWakefulnessLifecycle, mMainExecutor,
- mBgExecutor, mKeyguardUpdateMonitor, mLogger)
+ mContext,
+ mContext.getResources(),
+ mWifiRepository,
+ mSatelliteViewModel,
+ mJavaAdapter,
+ mTelephonyManager,
+ mTelephonyListenerManager,
+ mWakefulnessLifecycle,
+ mMainExecutor,
+ mBgExecutor,
+ mKeyguardUpdateMonitor,
+ mLogger)
.setShowAirplaneMode(true)
.setShowMissingSim(true)
.build();
@@ -211,6 +231,8 @@
getContextSpyForStickyBroadcast(stickyIntent),
mContext.getResources(),
mWifiRepository,
+ mSatelliteViewModel,
+ mJavaAdapter,
mTelephonyManager,
mTelephonyListenerManager,
mWakefulnessLifecycle,
@@ -451,6 +473,107 @@
}
@Test
+ public void carrierText_satelliteTextNull_notUsed() {
+ reset(mCarrierTextCallback);
+ List<SubscriptionInfo> list = new ArrayList<>();
+ list.add(TEST_SUBSCRIPTION);
+ when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+ TelephonyManager.SIM_STATE_READY);
+ when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+ mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+ // WHEN the satellite text is null
+ mSatelliteViewModel.getCarrierText().setValue(null);
+ mTestScope.getTestScheduler().runCurrent();
+
+ ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+ ArgumentCaptor.forClass(
+ CarrierTextManager.CarrierTextCallbackInfo.class);
+ FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+
+ // THEN the default subscription carrier text is used
+ verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+ assertThat(captor.getValue().carrierText).isEqualTo(TEST_CARRIER);
+ }
+
+ @Test
+ public void carrierText_satelliteTextUpdates_autoTriggersCallback() {
+ reset(mCarrierTextCallback);
+ List<SubscriptionInfo> list = new ArrayList<>();
+ list.add(TEST_SUBSCRIPTION);
+ when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+ TelephonyManager.SIM_STATE_READY);
+ when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+ mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+ // WHEN the satellite text is set
+ mSatelliteViewModel.getCarrierText().setValue("Test satellite text");
+ mTestScope.getTestScheduler().runCurrent();
+
+ // THEN we should automatically re-trigger #updateCarrierText and get callback info
+ ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+ ArgumentCaptor.forClass(
+ CarrierTextManager.CarrierTextCallbackInfo.class);
+ FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+ verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+ // AND use the satellite text as the carrier text
+ assertThat(captor.getValue().carrierText).isEqualTo("Test satellite text");
+
+ // WHEN the satellite text is reset to null
+ reset(mCarrierTextCallback);
+ mSatelliteViewModel.getCarrierText().setValue(null);
+ mTestScope.getTestScheduler().runCurrent();
+
+ // THEN we should automatically re-trigger #updateCarrierText and get callback info
+ // that doesn't include the satellite info
+ FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+ verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+ assertThat(captor.getValue().carrierText).isEqualTo(TEST_CARRIER);
+ }
+
+ @Test
+ public void carrierText_updatedWhileNotListening_getsNewValueWhenListening() {
+ reset(mCarrierTextCallback);
+ List<SubscriptionInfo> list = new ArrayList<>();
+ list.add(TEST_SUBSCRIPTION);
+ when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+ TelephonyManager.SIM_STATE_READY);
+ when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+ mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+ mSatelliteViewModel.getCarrierText().setValue("Old satellite text");
+ mTestScope.getTestScheduler().runCurrent();
+
+ ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+ ArgumentCaptor.forClass(
+ CarrierTextManager.CarrierTextCallbackInfo.class);
+ FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+ verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+ assertThat(captor.getValue().carrierText).isEqualTo("Old satellite text");
+
+ // WHEN we stop listening
+ reset(mCarrierTextCallback);
+ mCarrierTextManager.setListening(null);
+
+ // AND the satellite text updates
+ mSatelliteViewModel.getCarrierText().setValue("New satellite text");
+
+ // THEN we don't get new callback info because we aren't listening
+ verify(mCarrierTextCallback, never()).updateCarrierInfo(any());
+
+ // WHEN we start listening again
+ reset(mCarrierTextCallback);
+ mCarrierTextManager.setListening(mCarrierTextCallback);
+
+ // THEN we should automatically re-trigger #updateCarrierText and get callback info
+ // that includes the new satellite text
+ mTestScope.getTestScheduler().runCurrent();
+ FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+ verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+ assertThat(captor.getValue().carrierText).isEqualTo("New satellite text");
+ }
+
+ @Test
public void testCreateInfo_noSubscriptions() {
reset(mCarrierTextCallback);
when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(
@@ -471,6 +594,28 @@
}
@Test
+ public void testCarrierText_oneValidSubscription() {
+ reset(mCarrierTextCallback);
+ List<SubscriptionInfo> list = new ArrayList<>();
+ list.add(TEST_SUBSCRIPTION);
+ when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+ TelephonyManager.SIM_STATE_READY);
+ when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+
+ mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+ ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+ ArgumentCaptor.forClass(
+ CarrierTextManager.CarrierTextCallbackInfo.class);
+
+ mCarrierTextManager.updateCarrierText();
+ FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+ verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+
+ assertThat(captor.getValue().carrierText).isEqualTo(TEST_CARRIER);
+ }
+
+ @Test
public void testCarrierText_twoValidSubscriptions() {
reset(mCarrierTextCallback);
List<SubscriptionInfo> list = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 68b6d9d..d354fa2 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -2016,6 +2016,16 @@
}
@Test
+ public void unfoldFromPostureChange_sendActionToFaceAuthInteractor() {
+ // WHEN device posture changes to unfold
+ deviceInPostureStateOpened();
+ mTestableLooper.processAllMessages();
+
+ // THEN request face auth
+ verify(mFaceAuthInteractor).onDeviceUnfolded();
+ }
+
+ @Test
public void detectFingerprint_onTemporaryLockoutReset_authenticateFingerprint() {
ArgumentCaptor<FingerprintManager.LockoutResetCallback> fpLockoutResetCallbackCaptor =
ArgumentCaptor.forClass(FingerprintManager.LockoutResetCallback.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
index e157fc5..4f7610a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt
@@ -27,7 +27,6 @@
import com.android.systemui.flags.systemPropertiesHelper
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.process.processWrapper
-import com.android.systemui.startable.Dependencies
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import javax.inject.Provider
@@ -56,6 +55,19 @@
@Mock private lateinit var bootCompleteCache: BootCompleteCacheImpl
@Mock private lateinit var initController: InitController
+ class StartableA : TestableStartable()
+ class StartableB : TestableStartable()
+ class StartableC : TestableStartable()
+ class StartableD : TestableStartable()
+ class StartableE : TestableStartable()
+
+ val dependencyMap: Map<Class<*>, Set<Class<out CoreStartable>>> =
+ mapOf(
+ StartableC::class.java to setOf(StartableA::class.java),
+ StartableD::class.java to setOf(StartableA::class.java, StartableB::class.java),
+ StartableE::class.java to setOf(StartableD::class.java, StartableB::class.java),
+ )
+
private val startableA = StartableA()
private val startableB = StartableB()
private val startableC = StartableC()
@@ -76,6 +88,7 @@
whenever(sysuiComponent.provideBootCacheImpl()).thenReturn(bootCompleteCache)
whenever(sysuiComponent.createDumpManager()).thenReturn(kosmos.dumpManager)
whenever(sysuiComponent.initController).thenReturn(initController)
+ whenever(sysuiComponent.startableDependencies).thenReturn(dependencyMap)
kosmos.processWrapper.systemUser = true
app.setContextAvailableCallback(contextAvailableCallback)
@@ -168,13 +181,4 @@
startOrder++
}
}
-
- class StartableA : TestableStartable()
- class StartableB : TestableStartable()
-
- @Dependencies(StartableA::class) class StartableC : TestableStartable()
-
- @Dependencies(StartableA::class, StartableB::class) class StartableD : TestableStartable()
-
- @Dependencies(StartableD::class, StartableB::class) class StartableE : TestableStartable()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt
deleted file mode 100644
index c22d35c..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.haptics.slider
-
-import android.widget.SeekBar
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import junit.framework.Assert.assertEquals
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class SeekableSliderEventProducerTest : SysuiTestCase() {
-
- private val seekBar = SeekBar(mContext)
- private val eventProducer = SeekableSliderEventProducer()
- private val eventFlow = eventProducer.produceEvents()
-
- @Test
- fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest {
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onStartTrackingTouch(seekBar)
-
- assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest)
- }
-
- @Test
- fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest {
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onStopTrackingTouch(seekBar)
-
- assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest)
- }
-
- @Test
- fun onProgressChangeByUser_changeByUserEventProduced_withNormalizedProgress() = runTest {
- val progress = 50
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onProgressChanged(seekBar, progress, true)
-
- assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5F), latest)
- }
-
- @Test
- fun onProgressChangeByUser_zeroWidthSlider_changeByUserEventProduced_withMaxProgress() =
- runTest {
- // No-width slider where the min and max values are the same
- seekBar.min = 100
- seekBar.max = 100
- val progress = 50
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onProgressChanged(seekBar, progress, true)
-
- assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 1.0F), latest)
- }
-
- @Test
- fun onProgressChangeByProgram_changeByProgramEventProduced_withNormalizedProgress() = runTest {
- val progress = 50
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onProgressChanged(seekBar, progress, false)
-
- assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 0.5F), latest)
- }
-
- @Test
- fun onProgressChangeByProgram_zeroWidthSlider_changeByProgramEventProduced_withMaxProgress() =
- runTest {
- // No-width slider where the min and max values are the same
- seekBar.min = 100
- seekBar.max = 100
- val progress = 50
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onProgressChanged(seekBar, progress, false)
-
- assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 1.0F), latest)
- }
-
- @Test
- fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced_withNormalizedProgress() =
- runTest {
- val progress = 50
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onProgressChanged(seekBar, progress, true)
- eventProducer.onStartTrackingTouch(seekBar)
-
- assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5F), latest)
- }
-
- @Test
- fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced_withNormalizedProgress() =
- runTest {
- val progress = 50
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onProgressChanged(seekBar, progress, true)
- eventProducer.onStopTrackingTouch(seekBar)
-
- assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5F), latest)
- }
-
- @Test
- fun onArrowUp_afterStartTrackingTouch_ArrowUpProduced() = runTest {
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onStartTrackingTouch(seekBar)
- eventProducer.onArrowUp()
-
- assertEquals(SliderEvent(SliderEventType.ARROW_UP, 0f), latest)
- }
-
- @Test
- fun onArrowUp_afterChangeByProgram_ArrowUpProduced_withProgress() = runTest {
- val progress = 50
- val latest by collectLastValue(eventFlow)
-
- eventProducer.onProgressChanged(seekBar, progress, false)
- eventProducer.onArrowUp()
-
- assertEquals(SliderEvent(SliderEventType.ARROW_UP, 0.5f), latest)
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt
similarity index 79%
rename from packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt
index 796d6d9..a09d345 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt
@@ -38,11 +38,11 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
-class SeekableSliderTrackerTest : SysuiTestCase() {
+class SliderStateTrackerTest : SysuiTestCase() {
@Mock private lateinit var sliderStateListener: SliderStateListener
private val sliderEventProducer = FakeSliderEventProducer()
- private lateinit var mSeekableSliderTracker: SeekableSliderTracker
+ private lateinit var mSliderStateTracker: SliderStateTracker
@Before
fun setup() {
@@ -55,7 +55,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN the tracker job is active
- assertThat(mSeekableSliderTracker.isTracking).isTrue()
+ assertThat(mSliderStateTracker.isTracking).isTrue()
}
@Test
@@ -65,14 +65,14 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a state in the state machine
- mSeekableSliderTracker.setState(it)
+ mSliderStateTracker.setState(it)
// WHEN the tracker stops tracking the state and listening to events
- mSeekableSliderTracker.stopTracking()
+ mSliderStateTracker.stopTracking()
// THEN The state is idle and the tracker is not active
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
- assertThat(mSeekableSliderTracker.isTracking).isFalse()
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.isTracking).isFalse()
}
}
@@ -83,7 +83,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN The state is idle and the listener is not called to play haptics
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
verifyZeroInteractions(sliderStateListener)
}
@@ -96,9 +96,9 @@
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress))
// THEN the tracker moves to the wait state and the timer job begins
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT)
verifyZeroInteractions(sliderStateListener)
- assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+ assertThat(mSliderStateTracker.isWaiting).isTrue()
}
// Tests on the WAIT state
@@ -117,9 +117,9 @@
advanceTimeBy(config.waitTimeMillis + 10L)
// THEN the tracker moves to the DRAG_HANDLE_ACQUIRED_BY_TOUCH state
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
- assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+ assertThat(mSliderStateTracker.isWaiting).isFalse()
verify(sliderStateListener).onHandleAcquiredByTouch()
verifyNoMoreInteractions(sliderStateListener)
}
@@ -142,9 +142,9 @@
// THEN the tracker moves to the DRAG_HANDLE_ACQUIRED_BY_TOUCH state without the timer job
// being complete
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
- assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+ assertThat(mSliderStateTracker.isWaiting).isFalse()
verify(sliderStateListener).onHandleAcquiredByTouch()
verifyNoMoreInteractions(sliderStateListener)
}
@@ -166,9 +166,9 @@
)
// THEN the tracker moves to the jump-track location selected state
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.JUMP_TRACK_LOCATION_SELECTED)
- assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+ assertThat(mSliderStateTracker.isWaiting).isFalse()
verify(sliderStateListener).onProgressJump(anyFloat())
verifyNoMoreInteractions(sliderStateListener)
}
@@ -190,8 +190,8 @@
)
// THEN the tracker moves to the jump-track location selected state
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED)
- assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED)
+ assertThat(mSliderStateTracker.isWaiting).isFalse()
verifyNoMoreInteractions(sliderStateListener)
}
@@ -212,8 +212,8 @@
)
// THEN the tracker moves to the JUMP_TRACK_LOCATION_SELECTED state
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED)
- assertThat(mSeekableSliderTracker.isWaiting).isFalse()
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED)
+ assertThat(mSliderStateTracker.isWaiting).isFalse()
verifyNoMoreInteractions(sliderStateListener)
}
@@ -225,15 +225,15 @@
// GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the
// slider
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5f))
- assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+ assertThat(mSliderStateTracker.isWaiting).isTrue()
// GIVEN that the tracker stops tracking the state and listening to events
- mSeekableSliderTracker.stopTracking()
+ mSliderStateTracker.stopTracking()
// THEN the tracker moves to the IDLE state without the timer job being complete
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
- assertThat(mSeekableSliderTracker.isWaiting).isFalse()
- assertThat(mSeekableSliderTracker.isTracking).isFalse()
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.isWaiting).isFalse()
+ assertThat(mSliderStateTracker.isTracking).isFalse()
verifyNoMoreInteractions(sliderStateListener)
}
@@ -244,13 +244,13 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_TRACK_LOCATION_SELECTED state
- mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
+ mSliderStateTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
// GIVEN a progress event due to dragging the handle
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5f))
// THEN the tracker moves to the DRAG_HANDLE_DRAGGING state
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
verify(sliderStateListener).onProgress(anyFloat())
verifyNoMoreInteractions(sliderStateListener)
}
@@ -260,14 +260,14 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_TRACK_LOCATION_SELECTED state
- mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
+ mSliderStateTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED)
// GIVEN that the slider stopped tracking touch
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
// THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
verify(sliderStateListener).onHandleReleasedFromTouch()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -276,13 +276,13 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_BOOKEND_SELECTED state
- mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
+ mSliderStateTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
// GIVEN that the slider stopped tracking touch
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5f))
// THEN the tracker moves to the DRAG_HANDLE_DRAGGING state
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
verify(sliderStateListener).onProgress(anyFloat())
verifyNoMoreInteractions(sliderStateListener)
}
@@ -292,14 +292,14 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a JUMP_BOOKEND_SELECTED state
- mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
+ mSliderStateTracker.setState(SliderState.JUMP_BOOKEND_SELECTED)
// GIVEN that the slider stopped tracking touch
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
// THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
verify(sliderStateListener).onHandleReleasedFromTouch()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -310,7 +310,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
// GIVEN a progress change by the user
val progress = 0.5f
@@ -320,7 +320,7 @@
// THEN the tracker moves to the DRAG_HANDLE_DRAGGING state
verify(sliderStateListener).onProgress(progress)
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -329,14 +329,14 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH)
// GIVEN that the handle stops tracking touch
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
// THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
verify(sliderStateListener).onHandleReleasedFromTouch()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -348,7 +348,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_DRAGGING state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
// GIVEN a progress change by the user outside of bookend bounds
val progress = 0.5f
@@ -357,8 +357,7 @@
)
// THEN the tracker does not change state and executes the onProgress call
- assertThat(mSeekableSliderTracker.currentState)
- .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
verify(sliderStateListener).onProgress(progress)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -370,7 +369,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_DRAGGING state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
// GIVEN a progress change by the user reaching the lower bookend
val progress = config.lowerBookendThreshold - 0.01f
@@ -380,7 +379,7 @@
// THEN the tracker moves to the DRAG_HANDLE_REACHED_BOOKEND state and executes the
// corresponding callback
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
verify(sliderStateListener).onLowerBookend()
verifyNoMoreInteractions(sliderStateListener)
@@ -393,7 +392,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_DRAGGING state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
// GIVEN a progress change by the user reaching the upper bookend
val progress = config.upperBookendThreshold + 0.01f
@@ -403,7 +402,7 @@
// THEN the tracker moves to the DRAG_HANDLE_REACHED_BOOKEND state and executes the
// corresponding callback
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
verify(sliderStateListener).onUpperBookend()
verifyNoMoreInteractions(sliderStateListener)
@@ -414,14 +413,14 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_DRAGGING state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING)
// GIVEN that the slider stops tracking touch
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
// THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
verify(sliderStateListener).onHandleReleasedFromTouch()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -434,7 +433,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
// GIVEN a progress event that falls outside of the lower bookend range
val progress = config.lowerBookendThreshold + 0.01f
@@ -444,8 +443,7 @@
// THEN the tracker moves to the DRAG_HANDLE_DRAGGING state and executes accordingly
verify(sliderStateListener).onProgress(progress)
- assertThat(mSeekableSliderTracker.currentState)
- .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -455,7 +453,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
// GIVEN a progress event that falls inside of the lower bookend range
val progress = config.lowerBookendThreshold - 0.01f
@@ -465,7 +463,7 @@
// THEN the tracker stays in the current state and executes accordingly
verify(sliderStateListener).onLowerBookend()
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -477,7 +475,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
// GIVEN a progress event that falls outside of the upper bookend range
val progress = config.upperBookendThreshold - 0.01f
@@ -487,8 +485,7 @@
// THEN the tracker moves to the DRAG_HANDLE_DRAGGING state and executes accordingly
verify(sliderStateListener).onProgress(progress)
- assertThat(mSeekableSliderTracker.currentState)
- .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -498,7 +495,7 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
// GIVEN a progress event that falls inside of the upper bookend range
val progress = config.upperBookendThreshold + 0.01f
@@ -508,7 +505,7 @@
// THEN the tracker stays in the current state and executes accordingly
verify(sliderStateListener).onUpperBookend()
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
verifyNoMoreInteractions(sliderStateListener)
}
@@ -518,37 +515,36 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a DRAG_HANDLE_REACHED_BOOKEND state
- mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
+ mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND)
// GIVEN that the handle stops tracking touch
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f))
// THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state
verify(sliderStateListener).onHandleReleasedFromTouch()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
verifyNoMoreInteractions(sliderStateListener)
}
@Test
- fun onProgressChangeByProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest {
+ fun onStartedTrackingProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest {
// GIVEN an initialized tracker in the IDLE state
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// GIVEN a progress due to an external source that lands at the middle of the slider
val progress = 0.5f
sliderEventProducer.sendEvent(
- SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress)
)
// THEN the state moves to ARROW_HANDLE_MOVED_ONCE and the listener is called to play
// haptics
- assertThat(mSeekableSliderTracker.currentState)
- .isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
verify(sliderStateListener).onSelectAndArrow(progress)
}
@Test
- fun onProgressChangeByProgram_atUpperBookend_onIdle_movesToIdle() = runTest {
+ fun onStartedTrackingProgram_atUpperBookend_onIdle_movesToIdle() = runTest {
// GIVEN an initialized tracker in the IDLE state
val config = SeekableSliderTrackerConfig()
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
@@ -556,16 +552,16 @@
// GIVEN a progress due to an external source that lands at the upper bookend
val progress = config.upperBookendThreshold + 0.01f
sliderEventProducer.sendEvent(
- SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress)
)
// THEN the tracker executes upper bookend haptics before moving back to IDLE
verify(sliderStateListener).onUpperBookend()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
}
@Test
- fun onProgressChangeByProgram_atLowerBookend_onIdle_movesToIdle() = runTest {
+ fun onStartedTrackingProgram_atLowerBookend_onIdle_movesToIdle() = runTest {
// GIVEN an initialized tracker in the IDLE state
val config = SeekableSliderTrackerConfig()
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
@@ -573,26 +569,28 @@
// WHEN a progress is recorded due to an external source that lands at the lower bookend
val progress = config.lowerBookendThreshold - 0.01f
sliderEventProducer.sendEvent(
- SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress)
+ SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress)
)
// THEN the tracker executes lower bookend haptics before moving to IDLE
verify(sliderStateListener).onLowerBookend()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
}
@Test
fun onArrowUp_onArrowMovedOnce_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
- mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+ mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the external stimulus is released
val progress = 0.5f
- sliderEventProducer.sendEvent(SliderEvent(SliderEventType.ARROW_UP, progress))
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress)
+ )
// THEN the tracker moves back to IDLE and there are no haptics
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
verifyZeroInteractions(sliderStateListener)
}
@@ -600,7 +598,7 @@
fun onStartTrackingTouch_onArrowMovedOnce_movesToWait() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
- mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+ mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the slider starts tracking touch
val progress = 0.5f
@@ -608,8 +606,8 @@
// THEN the tracker moves back to WAIT and starts the waiting job. Also, there are no
// haptics
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT)
- assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT)
+ assertThat(mSliderStateTracker.isWaiting).isTrue()
verifyZeroInteractions(sliderStateListener)
}
@@ -617,7 +615,7 @@
fun onProgressChangeByProgram_onArrowMovedOnce_movesToArrowMovesContinuously() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
- mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
+ mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE)
// WHEN the slider gets an external progress change
val progress = 0.5f
@@ -627,7 +625,7 @@
// THEN the tracker moves to ARROW_HANDLE_MOVES_CONTINUOUSLY and calls the appropriate
// haptics
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
verify(sliderStateListener).onProgress(progress)
}
@@ -636,14 +634,16 @@
fun onArrowUp_onArrowMovesContinuously_movesToIdle() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
- mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+ mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the external stimulus is released
val progress = 0.5f
- sliderEventProducer.sendEvent(SliderEvent(SliderEventType.ARROW_UP, progress))
+ sliderEventProducer.sendEvent(
+ SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress)
+ )
// THEN the tracker moves to IDLE and no haptics are played
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
verifyZeroInteractions(sliderStateListener)
}
@@ -651,15 +651,15 @@
fun onStartTrackingTouch_onArrowMovesContinuously_movesToWait() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
- mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+ mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider starts tracking touch
val progress = 0.5f
sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress))
// THEN the tracker moves to WAIT and the wait job starts.
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT)
- assertThat(mSeekableSliderTracker.isWaiting).isTrue()
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT)
+ assertThat(mSliderStateTracker.isWaiting).isTrue()
verifyZeroInteractions(sliderStateListener)
}
@@ -667,7 +667,7 @@
fun onProgressChangeByProgram_onArrowMovesContinuously_preservesState() = runTest {
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
- mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+ mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider changes progress programmatically at the middle
val progress = 0.5f
@@ -676,7 +676,7 @@
)
// THEN the tracker stays in the same state and haptics are delivered appropriately
- assertThat(mSeekableSliderTracker.currentState)
+ assertThat(mSliderStateTracker.currentState)
.isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
verify(sliderStateListener).onProgress(progress)
}
@@ -686,7 +686,7 @@
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
val config = SeekableSliderTrackerConfig()
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
- mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+ mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider reaches the lower bookend programmatically
val progress = config.lowerBookendThreshold - 0.01f
@@ -696,7 +696,7 @@
// THEN the tracker executes lower bookend haptics before moving to IDLE
verify(sliderStateListener).onLowerBookend()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
}
@Test
@@ -704,7 +704,7 @@
// GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state
val config = SeekableSliderTrackerConfig()
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config)
- mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
+ mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY)
// WHEN the slider reaches the lower bookend programmatically
val progress = config.upperBookendThreshold + 0.01f
@@ -714,7 +714,7 @@
// THEN the tracker executes upper bookend haptics before moving to IDLE
verify(sliderStateListener).onUpperBookend()
- assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE)
+ assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE)
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -722,8 +722,8 @@
scope: CoroutineScope,
config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
) {
- mSeekableSliderTracker =
- SeekableSliderTracker(sliderStateListener, sliderEventProducer, scope, config)
- mSeekableSliderTracker.startTracking()
+ mSliderStateTracker =
+ SliderStateTracker(sliderStateListener, sliderEventProducer, scope, config)
+ mSliderStateTracker.startTracking()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
index 6a22d86..fb91c78 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt
@@ -24,7 +24,7 @@
import com.android.settingslib.RestrictedLockUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin
+import com.android.systemui.haptics.slider.SeekbarHapticPlugin
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.BrightnessMirrorController
@@ -93,7 +93,7 @@
brightnessSliderView,
mFalsingManager,
uiEventLogger,
- SeekableSliderHapticPlugin(vibratorHelper, systemClock),
+ SeekbarHapticPlugin(vibratorHelper, systemClock),
activityStarter,
)
mController.init()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index fd9daf8..03f5ecf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -25,23 +25,24 @@
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
+import com.android.systemui.ambient.touch.TouchHandler
+import com.android.systemui.ambient.touch.TouchMonitor
+import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
import com.android.systemui.communal.data.repository.FakeCommunalRepository
import com.android.systemui.communal.data.repository.fakeCommunalRepository
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -51,7 +52,6 @@
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
import com.android.systemui.shade.data.repository.fakeShadeRepository
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.testKosmos
@@ -60,7 +60,6 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertThrows
@@ -87,16 +86,14 @@
@Mock private lateinit var communalViewModel: CommunalViewModel
@Mock private lateinit var powerManager: PowerManager
@Mock private lateinit var dialogFactory: SystemUIDialogFactory
+ @Mock private lateinit var touchMonitor: TouchMonitor
@Mock private lateinit var communalColors: CommunalColors
- private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
- private lateinit var shadeInteractor: ShadeInteractor
- private lateinit var keyguardInteractor: KeyguardInteractor
+ private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory
private lateinit var parentView: FrameLayout
private lateinit var containerView: View
private lateinit var testableLooper: TestableLooper
- private lateinit var communalInteractor: CommunalInteractor
private lateinit var communalRepository: FakeCommunalRepository
private lateinit var underTest: GlanceableHubContainerController
@@ -104,32 +101,37 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
- communalInteractor = kosmos.communalInteractor
communalRepository = kosmos.fakeCommunalRepository
- keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
- keyguardInteractor = kosmos.keyguardInteractor
- shadeInteractor = kosmos.shadeInteractor
- underTest =
- GlanceableHubContainerController(
- communalInteractor,
- communalViewModel,
- dialogFactory,
- keyguardTransitionInteractor,
- keyguardInteractor,
- shadeInteractor,
- powerManager,
- communalColors,
- kosmos.sceneDataSourceDelegator,
- )
+ ambientTouchComponentFactory =
+ object : AmbientTouchComponent.Factory {
+ override fun create(
+ lifecycleOwner: LifecycleOwner,
+ touchHandlers: Set<TouchHandler>
+ ): AmbientTouchComponent =
+ object : AmbientTouchComponent {
+ override fun getTouchMonitor(): TouchMonitor = touchMonitor
+ }
+ }
+
+ with(kosmos) {
+ underTest =
+ GlanceableHubContainerController(
+ communalInteractor,
+ communalViewModel,
+ dialogFactory,
+ keyguardTransitionInteractor,
+ keyguardInteractor,
+ shadeInteractor,
+ powerManager,
+ communalColors,
+ ambientTouchComponentFactory,
+ kosmos.sceneDataSourceDelegator,
+ )
+ }
testableLooper = TestableLooper.get(this)
overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH)
- overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH)
- overrideResource(
- R.dimen.communal_bottom_edge_swipe_region_height,
- BOTTOM_SWIPE_REGION_WIDTH
- )
// Make communal available so that communalInteractor.desiredScene accurately reflects
// scene changes instead of just returning Blank.
@@ -161,6 +163,7 @@
shadeInteractor,
powerManager,
communalColors,
+ ambientTouchComponentFactory,
kosmos.sceneDataSourceDelegator,
)
@@ -215,62 +218,6 @@
}
@Test
- fun onTouchEvent_topSwipeWhenCommunalOpen_doesNotIntercept() =
- with(kosmos) {
- testScope.runTest {
- // Communal is open.
- goToScene(CommunalScenes.Communal)
-
- // Touch event in the top swipe region is not intercepted.
- assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
- }
- }
-
- @Test
- fun onTouchEvent_bottomSwipeWhenCommunalOpen_doesNotIntercept() =
- with(kosmos) {
- testScope.runTest {
- // Communal is open.
- goToScene(CommunalScenes.Communal)
-
- // Touch event in the bottom swipe region is not intercepted.
- assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
- }
- }
-
- @Test
- fun onTouchEvent_topSwipeWhenDreaming_doesNotIntercept() =
- with(kosmos) {
- testScope.runTest {
- // Communal is open.
- goToScene(CommunalScenes.Communal)
-
- // Device is dreaming.
- fakeKeyguardRepository.setDreaming(true)
- runCurrent()
-
- // Touch event in the top swipe region is not intercepted.
- assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
- }
- }
-
- @Test
- fun onTouchEvent_bottomSwipeWhenDreaming_doesNotIntercept() =
- with(kosmos) {
- testScope.runTest {
- // Communal is open.
- goToScene(CommunalScenes.Communal)
-
- // Device is dreaming.
- fakeKeyguardRepository.setDreaming(true)
- runCurrent()
-
- // Touch event in the bottom swipe region is not intercepted.
- assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
- }
- }
-
- @Test
fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() =
with(kosmos) {
testScope.runTest {
@@ -327,6 +274,141 @@
}
@Test
+ fun lifecycle_initializedAfterConstruction() =
+ with(kosmos) {
+ val underTest =
+ GlanceableHubContainerController(
+ communalInteractor,
+ communalViewModel,
+ dialogFactory,
+ keyguardTransitionInteractor,
+ keyguardInteractor,
+ shadeInteractor,
+ powerManager,
+ communalColors,
+ ambientTouchComponentFactory,
+ kosmos.sceneDataSourceDelegator,
+ )
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+ }
+
+ @Test
+ fun lifecycle_createdAfterViewCreated() =
+ with(kosmos) {
+ val underTest =
+ GlanceableHubContainerController(
+ communalInteractor,
+ communalViewModel,
+ dialogFactory,
+ keyguardTransitionInteractor,
+ keyguardInteractor,
+ shadeInteractor,
+ powerManager,
+ communalColors,
+ ambientTouchComponentFactory,
+ kosmos.sceneDataSourceDelegator,
+ )
+
+ // Only initView without attaching a view as we don't want the flows to start collecting
+ // yet.
+ underTest.initView(View(context))
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ }
+
+ @Test
+ fun lifecycle_startedAfterFlowsUpdate() {
+ // Flows start collecting due to test setup, causing the state to advance to STARTED.
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+
+ @Test
+ fun lifecycle_resumedAfterCommunalShows() {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
+ fun lifecycle_startedAfterCommunalCloses() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // Communal closes.
+ goToScene(CommunalScenes.Blank)
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+ }
+
+ @Test
+ fun lifecycle_startedAfterPrimaryBouncerShows() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ // Bouncer is visible.
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.PRIMARY_BOUNCER,
+ testScope
+ )
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+ }
+
+ @Test
+ fun lifecycle_startedAfterAlternateBouncerShows() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ // Bouncer is visible.
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.ALTERNATE_BOUNCER,
+ testScope
+ )
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+ }
+
+ @Test
+ fun lifecycle_createdAfterDisposeView() {
+ // Container view disposed.
+ underTest.disposeView()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+ }
+
+ @Test
+ fun lifecycle_startedAfterShadeShows() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ // Shade shows up.
+ fakeShadeRepository.setQsExpansion(1.0f)
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+ }
+
+ @Test
fun editMode_communalAvailable() =
with(kosmos) {
testScope.runTest {
@@ -371,8 +453,6 @@
private const val CONTAINER_WIDTH = 100
private const val CONTAINER_HEIGHT = 100
private const val RIGHT_SWIPE_REGION_WIDTH = 20
- private const val TOP_SWIPE_REGION_WIDTH = 20
- private const val BOTTOM_SWIPE_REGION_WIDTH = 20
/**
* A touch down event right in the middle of the screen, to avoid being in any of the swipe
@@ -389,17 +469,6 @@
)
private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT =
MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0)
- private val DOWN_IN_TOP_SWIPE_REGION_EVENT =
- MotionEvent.obtain(
- 0L,
- 0L,
- MotionEvent.ACTION_DOWN,
- 0f,
- TOP_SWIPE_REGION_WIDTH.toFloat(),
- 0
- )
- private val DOWN_IN_BOTTOM_SWIPE_REGION_EVENT =
- MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, CONTAINER_HEIGHT.toFloat(), 0)
private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index da09579..d95cc2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -500,6 +500,46 @@
}
@Test
+ fun handleExternalTouch_intercepted_sendsOnTouch() {
+ // Accept dispatch and also intercept.
+ whenever(view.dispatchTouchEvent(any())).thenReturn(true)
+ whenever(view.onInterceptTouchEvent(any())).thenReturn(true)
+
+ underTest.handleExternalTouch(DOWN_EVENT)
+ underTest.handleExternalTouch(MOVE_EVENT)
+
+ // Once intercepted, both events are sent to the view.
+ verify(view).onTouchEvent(DOWN_EVENT)
+ verify(view).onTouchEvent(MOVE_EVENT)
+ }
+
+ @Test
+ fun handleExternalTouch_notDispatched_interceptNotCalled() {
+ // Don't accept dispatch
+ whenever(view.dispatchTouchEvent(any())).thenReturn(false)
+
+ underTest.handleExternalTouch(DOWN_EVENT)
+
+ // Interception is not offered.
+ verify(view, never()).onInterceptTouchEvent(any())
+ }
+
+ @Test
+ fun handleExternalTouch_notIntercepted_onTouchNotSent() {
+ // Accept dispatch, but don't dispatch
+ whenever(view.dispatchTouchEvent(any())).thenReturn(true)
+ whenever(view.onInterceptTouchEvent(any())).thenReturn(false)
+
+ underTest.handleExternalTouch(DOWN_EVENT)
+ underTest.handleExternalTouch(MOVE_EVENT)
+
+ // Interception offered for both events, but onTouchEvent is never called.
+ verify(view).onInterceptTouchEvent(DOWN_EVENT)
+ verify(view).onInterceptTouchEvent(MOVE_EVENT)
+ verify(view, never()).onTouchEvent(any())
+ }
+
+ @Test
fun testGetKeyguardMessageArea() =
testScope.runTest {
underTest.keyguardMessageArea
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index 64f19b6..8eea29b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -21,11 +21,13 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.log.core.FakeLogBuffer
+import com.android.systemui.res.R
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
@@ -76,7 +78,8 @@
)
underTest =
- DeviceBasedSatelliteViewModel(
+ DeviceBasedSatelliteViewModelImpl(
+ context,
interactor,
testScope.backgroundScope,
airplaneModeRepository,
@@ -124,6 +127,7 @@
assertThat(latest).isNull()
}
+ @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun icon_nullWhenShouldNotShow_isEmergencyOnly() =
testScope.runTest {
@@ -298,4 +302,313 @@
// THEN icon is set because the device lost wifi connection
assertThat(latest).isInstanceOf(Icon::class.java)
}
+
+ @Test
+ fun carrierText_nullWhenShouldNotShow_satelliteNotAllowed() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is not allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = false
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // THEN carrier text is null because we should not be showing it
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun carrierText_nullWhenShouldNotShow_notAllOos() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is allowed + connected
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // GIVEN all icons are not OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = true
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // THEN carrier text is null because we have service
+ assertThat(latest).isNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_nullWhenShouldNotShow_isEmergencyOnly() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is allowed + connected
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ // THEN carrier text is set because we don't have service
+ assertThat(latest).isNotNull()
+
+ // GIVEN the connection is emergency only
+ i1.isEmergencyOnly.value = true
+
+ // THEN carrier text is null because we have emergency connection
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun carrierText_nullWhenShouldNotShow_apmIsEnabled() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is allowed + connected
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is enabled
+ airplaneModeRepository.setIsAirplaneMode(true)
+
+ // THEN carrier text is null because we should not be showing it
+ assertThat(latest).isNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_satelliteIsOn() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is allowed + connected
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ // THEN carrier text is set because we don't have service
+ assertThat(latest).isNotNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_hysteresisWhenEnablingText() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is allowed + connected
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // THEN carrier text is null because of the hysteresis
+ assertThat(latest).isNull()
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ // THEN carrier text is set after the delay
+ assertThat(latest).isNotNull()
+
+ // GIVEN apm is enabled
+ airplaneModeRepository.setIsAirplaneMode(true)
+
+ // THEN carrier text is null immediately
+ assertThat(latest).isNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_deviceIsProvisioned() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is allowed + connected
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // GIVEN device is not provisioned
+ deviceProvisionedRepository.setDeviceProvisioned(false)
+
+ // THEN carrier text is null because the device is not provisioned
+ assertThat(latest).isNull()
+
+ // GIVEN device becomes provisioned
+ deviceProvisionedRepository.setDeviceProvisioned(true)
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ // THEN carrier text is null because the device is not provisioned
+ assertThat(latest).isNotNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_wifiIsActive() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // GIVEN satellite is allowed + connected
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // GIVEN all icons are OOS
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+
+ // GIVEN apm is disabled
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ // GIVEN device is provisioned
+ deviceProvisionedRepository.setDeviceProvisioned(true)
+
+ // GIVEN wifi network is active
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 0, level = 1))
+
+ // THEN carrier text is null because the device is connected to wifi
+ assertThat(latest).isNull()
+
+ // GIVEN device loses wifi connection
+ wifiRepository.setWifiNetwork(WifiNetworkModel.Invalid("test"))
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ // THEN carrier text is set because the device lost wifi connection
+ assertThat(latest).isNotNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_connectionStateUnknown_null() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // Set up the conditions for satellite to be enabled
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ repo.connectionState.value = SatelliteConnectionState.Unknown
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ assertThat(latest).isNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_connectionStateOff_null() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // Set up the conditions for satellite to be enabled
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ repo.connectionState.value = SatelliteConnectionState.Off
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ assertThat(latest).isNull()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_connectionStateOn_notConnectedString() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // Set up the conditions for satellite to be enabled
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ repo.connectionState.value = SatelliteConnectionState.On
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ assertThat(latest)
+ .isEqualTo(context.getString(R.string.satellite_not_connected_carrier_text))
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun carrierText_connectionStateConnected_connectedString() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.carrierText)
+
+ // Set up the conditions for satellite to be enabled
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+ val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1)
+ i1.isInService.value = false
+ i1.isEmergencyOnly.value = false
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ repo.connectionState.value = SatelliteConnectionState.Connected
+
+ // Wait for delay to be completed
+ advanceTimeBy(10.seconds)
+
+ assertThat(latest)
+ .isEqualTo(context.getString(R.string.satellite_connected_carrier_text))
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/FakeDeviceBasedSatelliteViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/FakeDeviceBasedSatelliteViewModel.kt
new file mode 100644
index 0000000..f125ef12
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/FakeDeviceBasedSatelliteViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeDeviceBasedSatelliteViewModel : DeviceBasedSatelliteViewModel {
+ override val icon = MutableStateFlow<Icon?>(null)
+ override val carrierText = MutableStateFlow<String?>(null)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
index 8a95136..65eb338 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
@@ -20,6 +20,7 @@
import android.graphics.Point
import com.android.systemui.biometrics.shared.model.LockoutMode
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.DevicePosture
import dagger.Binds
import dagger.Module
import javax.inject.Inject
@@ -42,6 +43,8 @@
override val cameraInfo: StateFlow<CameraInfo?>
get() = currentCameraInfo
+ override val supportedPostures: List<DevicePosture> = listOf(DevicePosture.CLOSED)
+
fun setLockoutMode(userId: Int, mode: LockoutMode) {
lockoutModesForUser[userId] = mode
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt
index 4e24233..e2a1fe4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt
@@ -58,3 +58,8 @@
*/
fun List<FlagsParameterization>.andSceneContainer(): List<FlagsParameterization> =
flatMap { it.andSceneContainer() }.toList()
+
+/** Parameterizes only the scene container flag. */
+fun parameterizeSceneContainerFlag(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index 162f278..d4b7937 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -22,6 +22,7 @@
import android.os.fakeExecutorHandler
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.bouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
import com.android.systemui.classifier.falsingCollector
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
@@ -30,8 +31,8 @@
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
-import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.globalactions.domain.interactor.globalActionsInteractor
import com.android.systemui.haptics.qs.qsLongPressEffect
import com.android.systemui.jank.interactionJankMonitor
@@ -41,6 +42,7 @@
import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.model.sceneContainerPlugin
import com.android.systemui.plugins.statusbar.statusBarStateController
@@ -50,6 +52,8 @@
import com.android.systemui.scene.sceneContainerConfig
import com.android.systemui.scene.shared.model.sceneDataSource
import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shadeController
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
import com.android.systemui.statusbar.phone.screenOffAnimationController
@@ -69,7 +73,6 @@
val testDispatcher by lazy { kosmos.testDispatcher }
val testScope by lazy { kosmos.testScope }
- val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic }
val fakeExecutor by lazy { kosmos.fakeExecutor }
val fakeExecutorHandler by lazy { kosmos.fakeExecutorHandler }
val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
@@ -77,6 +80,8 @@
val bouncerRepository by lazy { kosmos.bouncerRepository }
val communalRepository by lazy { kosmos.fakeCommunalRepository }
val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+ val keyguardBouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository }
+ val keyguardInteractor by lazy { kosmos.keyguardInteractor }
val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor }
val powerRepository by lazy { kosmos.fakePowerRepository }
@@ -91,6 +96,7 @@
val falsingCollector by lazy { kosmos.falsingCollector }
val powerInteractor by lazy { kosmos.powerInteractor }
val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
+ val deviceEntryUdfpsInteractor by lazy { kosmos.deviceEntryUdfpsInteractor }
val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
val communalInteractor by lazy { kosmos.communalInteractor }
val sceneContainerPlugin by lazy { kosmos.sceneContainerPlugin }
@@ -110,6 +116,8 @@
val brightnessMirrorShowingInteractor by lazy { kosmos.brightnessMirrorShowingInteractor }
val qsLongPressEffect by lazy { kosmos.qsLongPressEffect }
val shadeController by lazy { kosmos.shadeController }
+ val shadeRepository by lazy { kosmos.shadeRepository }
+ val shadeInteractor by lazy { kosmos.shadeInteractor }
init {
kosmos.applicationContext = testCase.context
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
index 63a05d7..6ac3a62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 5e566aa..522aa67 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -202,6 +202,7 @@
// Default max API calls per reset interval for generated preview API rate limiting.
private static final int DEFAULT_GENERATED_PREVIEW_MAX_CALLS_PER_INTERVAL = 2;
+ private static final String PENDING_DELETED_IDS_ATTR = "pending_deleted_ids";
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -228,6 +229,11 @@
onPackageBroadcastReceived(intent, getSendingUserId());
updateWidgetPackageSuspensionMaskedState(intent, false, getSendingUserId());
break;
+ case Intent.ACTION_PACKAGE_RESTARTED:
+ case Intent.ACTION_PACKAGE_UNSTOPPED:
+ if (!android.content.pm.Flags.stayStopped()) return;
+ updateWidgetPackageStoppedMaskedState(intent);
+ break;
default:
onPackageBroadcastReceived(intent, getSendingUserId());
break;
@@ -396,7 +402,10 @@
packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
packageFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_UNSTOPPED);
packageFilter.addDataScheme("package");
+ packageFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL,
packageFilter, null, mCallbackHandler);
@@ -581,14 +590,19 @@
changed |= provider.setMaskedByQuietProfileLocked(quietProfile);
try {
boolean suspended;
+ boolean stopped;
try {
suspended = mPackageManager.isPackageSuspendedForUser(
provider.id.componentName.getPackageName(), provider.getUserId());
+ stopped = mPackageManager.isPackageStoppedForUser(
+ provider.id.componentName.getPackageName(), provider.getUserId());
} catch (IllegalArgumentException ex) {
// Package not found.
suspended = false;
+ stopped = false;
}
changed |= provider.setMaskedBySuspendedPackageLocked(suspended);
+ changed |= provider.setMaskedByStoppedPackageLocked(stopped);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to query application info", e);
}
@@ -636,6 +650,82 @@
}
/**
+ * Update the masked state for a stopped or unstopped package.
+ */
+ private void updateWidgetPackageStoppedMaskedState(@NonNull Intent intent) {
+ final int providerUid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
+ final Uri uri = intent.getData();
+ if (providerUid == Process.INVALID_UID || uri == null) {
+ return;
+ }
+
+ final String packageName = uri.getSchemeSpecificPart();
+ if (packageName == null) {
+ return;
+ }
+
+ boolean isStopped;
+ try {
+ isStopped = mPackageManager.isPackageStoppedForUser(packageName,
+ UserHandle.getUserId(providerUid));
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to query package stopped state", e);
+ return;
+ }
+
+ if (DEBUG) {
+ Slog.i(TAG, "Updating package stopped masked state for uid " + providerUid + " package "
+ + packageName + " isStopped " + isStopped);
+ }
+ synchronized (mLock) {
+ final int count = mProviders.size();
+ for (int i = 0; i < count; i++) {
+ Provider provider = mProviders.get(i);
+ if (providerUid != provider.id.uid
+ || !packageName.equals(provider.id.componentName.getPackageName())) {
+ continue;
+ }
+ if (provider.setMaskedByStoppedPackageLocked(isStopped)) {
+ if (provider.isMaskedLocked()) {
+ maskWidgetsViewsLocked(provider, null);
+ cancelBroadcastsLocked(provider);
+ } else {
+ unmaskWidgetsViewsLocked(provider);
+ final int widgetCount = provider.widgets.size();
+ if (widgetCount > 0) {
+ final int[] widgetIds = new int[widgetCount];
+ for (int j = 0; j < widgetCount; j++) {
+ widgetIds[j] = provider.widgets.get(j).appWidgetId;
+ }
+ registerForBroadcastsLocked(provider, widgetIds);
+ sendUpdateIntentLocked(provider, widgetIds, /* interactive= */ false);
+ }
+
+ final int pendingIdsCount = provider.pendingDeletedWidgetIds.size();
+ if (pendingIdsCount > 0) {
+ if (DEBUG) {
+ Slog.i(TAG, "Sending missed deleted broadcasts for "
+ + provider.id.componentName + " "
+ + provider.pendingDeletedWidgetIds);
+ }
+ for (int j = 0; j < pendingIdsCount; j++) {
+ sendDeletedIntentLocked(provider.id.componentName,
+ provider.id.getProfile(),
+ provider.pendingDeletedWidgetIds.get(j));
+ }
+ provider.pendingDeletedWidgetIds.clear();
+ if (widgetCount == 0) {
+ sendDisabledIntentLocked(provider);
+ }
+ saveGroupStateAsync(provider.id.getProfile().getIdentifier());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
* Mask the target widget belonging to the specified provider, or all active widgets
* of the provider if target widget == null.
*/
@@ -648,11 +738,11 @@
R.layout.work_widget_mask_view);
ApplicationInfo appInfo = provider.info.providerInfo.applicationInfo;
final int appUserId = provider.getUserId();
- boolean showBadge;
+ boolean showBadge = false;
final long identity = Binder.clearCallingIdentity();
try {
- final Intent onClickIntent;
+ Intent onClickIntent = null;
if (provider.maskedByQuietProfile) {
showBadge = true;
@@ -676,7 +766,7 @@
appInfo.packageName, suspendingPackage, dialogInfo, null, null,
appUserId);
}
- } else /* provider.maskedByLockedProfile */ {
+ } else if (provider.maskedByLockedProfile) {
showBadge = true;
onClickIntent = mKeyguardManager
.createConfirmDeviceCredentialIntent(null, null, appUserId);
@@ -684,6 +774,8 @@
onClickIntent.setFlags(
FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
}
+ } else if (provider.maskedByStoppedPackage) {
+ showBadge = mUserManager.hasBadge(appUserId);
}
Icon icon = appInfo.icon != 0
@@ -697,7 +789,14 @@
for (int j = 0; j < widgetCount; j++) {
Widget widget = provider.widgets.get(j);
if (targetWidget != null && targetWidget != widget) continue;
- if (onClickIntent != null) {
+ if (provider.maskedByStoppedPackage) {
+ Intent intent = createUpdateIntentLocked(provider,
+ new int[] { widget.appWidgetId });
+ views.setOnClickPendingIntent(android.R.id.background,
+ PendingIntent.getBroadcast(mContext, widget.appWidgetId,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT
+ | PendingIntent.FLAG_IMMUTABLE));
+ } else if (onClickIntent != null) {
views.setOnClickPendingIntent(android.R.id.background,
PendingIntent.getActivity(mContext, widget.appWidgetId, onClickIntent,
PendingIntent.FLAG_UPDATE_CURRENT
@@ -1950,15 +2049,23 @@
if (provider != null) {
provider.widgets.remove(widget);
if (!provider.zombie) {
- // send the broacast saying that this appWidgetId has been deleted
- sendDeletedIntentLocked(widget);
+ // If the package is not stopped, send the broadcast saying that this appWidgetId
+ // has been deleted. Otherwise, save the ID and send the broadcast when the package
+ // is unstopped.
+ if (!provider.maskedByStoppedPackage) {
+ sendDeletedIntentLocked(widget);
+ } else {
+ provider.pendingDeletedWidgetIds.add(widget.appWidgetId);
+ }
if (provider.widgets.isEmpty()) {
// cancel the future updates
cancelBroadcastsLocked(provider);
- // send the broacast saying that the provider is not in use any more
- sendDisabledIntentLocked(provider);
+ // send the broadcast saying that the provider is not in use any more
+ if (!provider.maskedByStoppedPackage) {
+ sendDisabledIntentLocked(provider);
+ }
}
}
}
@@ -2033,8 +2140,9 @@
final HashSet<Integer> ids = mRemoteViewsServicesAppWidgets.get(key);
if (ids.remove(widget.appWidgetId)) {
// If we have removed the last app widget referencing this service, then we
- // should destroy it and remove it from this set
- if (ids.isEmpty()) {
+ // should destroy it and remove it from this set. This is skipped for widgets whose
+ // provider is in a stopped package, to avoid waking up the package.
+ if (ids.isEmpty() && !widget.provider.maskedByStoppedPackage) {
destroyRemoteViewsService(key.second.getIntent(), widget);
it.remove();
}
@@ -2544,18 +2652,29 @@
private void sendUpdateIntentLocked(Provider provider, int[] appWidgetIds,
boolean interactive) {
- Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
- intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
- intent.setComponent(provider.id.componentName);
+ Intent intent = createUpdateIntentLocked(provider, appWidgetIds);
sendBroadcastAsUser(intent, provider.id.getProfile(), interactive);
}
+ private Intent createUpdateIntentLocked(Provider provider, int[] appWidgetIds) {
+ Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ intent.setComponent(provider.id.componentName);
+ return intent;
+ }
+
private void sendDeletedIntentLocked(Widget widget) {
+ sendDeletedIntentLocked(widget.provider.id.componentName, widget.provider.id.getProfile(),
+ widget.appWidgetId);
+ }
+
+ private void sendDeletedIntentLocked(ComponentName provider, UserHandle profile,
+ int appWidgetId) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_DELETED);
- intent.setComponent(widget.provider.id.componentName);
- intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widget.appWidgetId);
+ intent.setComponent(provider);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
// Cleanup after deletion isn't an interactive UX case
- sendBroadcastAsUser(intent, widget.provider.id.getProfile(), false);
+ sendBroadcastAsUser(intent, profile, false);
}
private void sendDisabledIntentLocked(Provider provider) {
@@ -2684,6 +2803,14 @@
if (persistsProviderInfo && p.mInfoParsed) {
AppWidgetXmlUtil.writeAppWidgetProviderInfoLocked(out, p.info);
}
+ final int pendingIdsCount = p.pendingDeletedWidgetIds.size();
+ if (pendingIdsCount > 0) {
+ final List<String> idStrings = new ArrayList<>();
+ for (int i = 0; i < pendingIdsCount; i++) {
+ idStrings.add(String.valueOf(p.pendingDeletedWidgetIds.get(i)));
+ }
+ out.attribute(null, PENDING_DELETED_IDS_ATTR, String.join(",", idStrings));
+ }
out.endTag(null, "p");
}
@@ -3022,7 +3149,7 @@
continue;
}
- if (provider.widgets.size() > 0) {
+ if (provider.widgets.size() > 0 && !provider.maskedByStoppedPackage) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"appwidget init " + provider.id.componentName.getPackageName());
provider.widgets.forEach(widget -> {
@@ -3440,6 +3567,16 @@
legacyProviderIndex);
provider.tag = providerTag;
provider.infoTag = parser.getAttributeValue(null, "info_tag");
+
+ final String pendingDeletedIds = parser.getAttributeValue(null,
+ PENDING_DELETED_IDS_ATTR);
+ if (pendingDeletedIds != null && !pendingDeletedIds.isEmpty()) {
+ final String[] idStrings = pendingDeletedIds.split(",");
+ for (int i = 0; i < idStrings.length; i++) {
+ provider.pendingDeletedWidgetIds.add(
+ Integer.parseInt(idStrings[i]));
+ }
+ }
} else if ("h".equals(tag)) {
legacyHostIndex++;
Host host = new Host();
@@ -4443,6 +4580,11 @@
boolean maskedByLockedProfile;
boolean maskedByQuietProfile;
boolean maskedBySuspendedPackage;
+ // This provider's package has been stopped
+ boolean maskedByStoppedPackage;
+ // Widget IDs for which we haven't yet sent DELETED broadcasts because the package was
+ // stopped.
+ IntArray pendingDeletedWidgetIds = new IntArray();
boolean mInfoParsed = false;
@@ -4598,8 +4740,15 @@
return masked != oldState;
}
+ public boolean setMaskedByStoppedPackageLocked(boolean masked) {
+ boolean oldState = maskedByStoppedPackage;
+ maskedByStoppedPackage = masked;
+ return masked != oldState;
+ }
+
public boolean isMaskedLocked() {
- return maskedByQuietProfile || maskedByLockedProfile || maskedBySuspendedPackage;
+ return maskedByQuietProfile || maskedByLockedProfile || maskedBySuspendedPackage
+ || maskedByStoppedPackage;
}
public boolean shouldBePersisted() {
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index 6c7546e..64bca33 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -17,6 +17,7 @@
package com.android.server;
import static android.permission.flags.Flags.sensitiveContentImprovements;
+import static android.permission.flags.Flags.sensitiveContentMetricsBugfix;
import static android.permission.flags.Flags.sensitiveNotificationAppProtection;
import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
import static android.view.flags.Flags.sensitiveContentAppProtection;
@@ -93,7 +94,7 @@
private boolean mProjectionActive = false;
private static class MediaProjectionSession {
- private final int mUid;
+ private final int mUid; // UID of app that started projection session
private final long mSessionId;
private final boolean mIsExempted;
private final ArraySet<String> mAllSeenNotificationKeys = new ArraySet<>();
@@ -320,6 +321,12 @@
}
mProjectionActive = true;
+
+ if (sensitiveContentMetricsBugfix()) {
+ mWindowManager.setBlockScreenCaptureForAppsSessionId(
+ mMediaProjectionSession.mSessionId);
+ }
+
if (sensitiveNotificationAppProtection()) {
updateAppsThatShouldBlockScreenCapture();
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bd67cf420..e171064 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -425,6 +425,8 @@
private int[] mSCBMReason;
private boolean[] mSCBMStarted;
+ private boolean[] mCarrierRoamingNtnMode = null;
+
/**
* Per-phone map of precise data connection state. The key of the map is the pair of transport
* type and APN setting. This is the cache to prevent redundant callbacks to the listeners.
@@ -723,6 +725,7 @@
mECBMStarted = copyOf(mECBMStarted, mNumPhones);
mSCBMReason = copyOf(mSCBMReason, mNumPhones);
mSCBMStarted = copyOf(mSCBMStarted, mNumPhones);
+ mCarrierRoamingNtnMode = copyOf(mCarrierRoamingNtnMode, mNumPhones);
// ds -> ss switch.
if (mNumPhones < oldNumPhones) {
cutListToSize(mCellInfo, mNumPhones);
@@ -781,6 +784,7 @@
mECBMStarted[i] = false;
mSCBMReason[i] = TelephonyManager.STOP_REASON_UNKNOWN;
mSCBMStarted[i] = false;
+ mCarrierRoamingNtnMode[i] = false;
}
}
}
@@ -854,6 +858,7 @@
mECBMStarted = new boolean[numPhones];
mSCBMReason = new int[numPhones];
mSCBMStarted = new boolean[numPhones];
+ mCarrierRoamingNtnMode = new boolean[numPhones];
for (int i = 0; i < numPhones; i++) {
mCallState[i] = TelephonyManager.CALL_STATE_IDLE;
@@ -897,6 +902,7 @@
mECBMStarted[i] = false;
mSCBMReason[i] = TelephonyManager.STOP_REASON_UNKNOWN;
mSCBMStarted[i] = false;
+ mCarrierRoamingNtnMode[i] = false;
}
mAppOps = mContext.getSystemService(AppOpsManager.class);
@@ -1523,6 +1529,14 @@
remove(r.binder);
}
}
+ if (events.contains(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED)) {
+ try {
+ r.callback.onCarrierRoamingNtnModeChanged(
+ mCarrierRoamingNtnMode[r.phoneId]);
+ } catch (RemoteException ex) {
+ remove(r.binder);
+ }
+ }
}
}
}
@@ -3512,6 +3526,41 @@
handleRemoveListLocked();
}
+ /**
+ * Notify external listeners that carrier roaming non-terrestrial network mode changed.
+ * @param subId subscription ID.
+ * @param active {@code true} If the device is connected to carrier roaming
+ * non-terrestrial network or was connected within the
+ * {CarrierConfigManager#KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT}
+ * duration, {code false} otherwise.
+ */
+ public void notifyCarrierRoamingNtnModeChanged(int subId, boolean active) {
+ if (!checkNotifyPermission("notifyCarrierRoamingNtnModeChanged")) {
+ return;
+ }
+
+ if (VDBG) {
+ log("notifyCarrierRoamingNtnModeChanged: subId=" + subId + " active=" + active);
+ }
+
+ synchronized (mRecords) {
+ int phoneId = getPhoneIdFromSubId(subId);
+ mCarrierRoamingNtnMode[phoneId] = active;
+ for (Record r : mRecords) {
+ if (r.matchTelephonyCallbackEvent(
+ TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED)
+ && idMatch(r, subId, phoneId)) {
+ try {
+ r.callback.onCarrierRoamingNtnModeChanged(active);
+ } catch (RemoteException ex) {
+ mRemoveList.add(r.binder);
+ }
+ }
+ }
+ handleRemoveListLocked();
+ }
+ }
+
@NeverCompile // Avoid size overhead of debugging code.
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index c3a3bf7..eec22c9 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -2422,8 +2422,6 @@
getTimeLimitedFgsType(foregroundServiceType);
final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedFgsType);
if (fgsTypeInfo != null) {
- // TODO(b/330399444): check to see if all time book-keeping for
- // time limited types should use elapsedRealtime instead of uptime
final long before24Hr = Math.max(0,
SystemClock.elapsedRealtime() - (24 * 60 * 60 * 1000));
final long lastTimeOutAt = fgsTypeInfo.getTimeLimitExceededAt();
@@ -3757,7 +3755,7 @@
}
traceInstant("FGS start: ", sr);
- final long nowUptime = SystemClock.uptimeMillis();
+ final long nowRealtime = SystemClock.elapsedRealtime();
// Fetch/create/update the fgs info for the time-limited type.
SparseArray<TimeLimitedFgsInfo> fgsInfo = mTimeLimitedFgsInfo.get(sr.appInfo.uid);
@@ -3768,10 +3766,10 @@
final int timeLimitedFgsType = getTimeLimitedFgsType(sr.foregroundServiceType);
TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedFgsType);
if (fgsTypeInfo == null) {
- fgsTypeInfo = sr.createTimeLimitedFgsInfo(nowUptime);
+ fgsTypeInfo = sr.createTimeLimitedFgsInfo(nowRealtime);
fgsInfo.put(timeLimitedFgsType, fgsTypeInfo);
}
- fgsTypeInfo.setLastFgsStartTime(nowUptime);
+ fgsTypeInfo.setLastFgsStartTime(nowRealtime);
// We'll cancel the previous ANR timer and start a fresh one below.
mFGSAnrTimer.cancel(sr);
@@ -3845,14 +3843,14 @@
final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(fgsType);
if (fgsTypeInfo != null) {
// Update total runtime for the time-limited fgs type and mark it as timed out.
- final long nowUptime = SystemClock.uptimeMillis();
+ final long nowRealtime = SystemClock.elapsedRealtime();
fgsTypeInfo.updateTotalRuntime();
- fgsTypeInfo.setTimeLimitExceededAt(nowUptime);
+ fgsTypeInfo.setTimeLimitExceededAt(nowRealtime);
logFGSStateChangeLocked(sr,
FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT,
- nowUptime > fgsTypeInfo.getLastFgsStartTime()
- ? (int) (nowUptime - fgsTypeInfo.getLastFgsStartTime()) : 0,
+ nowRealtime > fgsTypeInfo.getLastFgsStartTime()
+ ? (int) (nowRealtime - fgsTypeInfo.getLastFgsStartTime()) : 0,
FGS_STOP_REASON_UNKNOWN,
FGS_TYPE_POLICY_CHECK_UNKNOWN,
FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA,
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 045d137..065f3bd 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -30,9 +30,9 @@
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.UptimeMillisLong;
import android.app.BackgroundStartPrivileges;
import android.app.IApplicationThread;
import android.app.Notification;
@@ -677,46 +677,46 @@
* Data container class to help track certain fgs info for time-restricted types.
*/
static class TimeLimitedFgsInfo {
- @UptimeMillisLong
+ @ElapsedRealtimeLong
private long mFirstFgsStartTime;
- @UptimeMillisLong
+ @ElapsedRealtimeLong
private long mLastFgsStartTime;
- @UptimeMillisLong
+ @ElapsedRealtimeLong
private long mTimeLimitExceededAt = Long.MIN_VALUE;
private long mTotalRuntime = 0;
- TimeLimitedFgsInfo(@UptimeMillisLong long startTime) {
+ TimeLimitedFgsInfo(@ElapsedRealtimeLong long startTime) {
mFirstFgsStartTime = startTime;
mLastFgsStartTime = startTime;
}
- @UptimeMillisLong
+ @ElapsedRealtimeLong
public long getFirstFgsStartTime() {
return mFirstFgsStartTime;
}
- public void setLastFgsStartTime(@UptimeMillisLong long startTime) {
+ public void setLastFgsStartTime(@ElapsedRealtimeLong long startTime) {
mLastFgsStartTime = startTime;
}
- @UptimeMillisLong
+ @ElapsedRealtimeLong
public long getLastFgsStartTime() {
return mLastFgsStartTime;
}
public void updateTotalRuntime() {
- mTotalRuntime += SystemClock.uptimeMillis() - mLastFgsStartTime;
+ mTotalRuntime += SystemClock.elapsedRealtime() - mLastFgsStartTime;
}
public long getTotalRuntime() {
return mTotalRuntime;
}
- public void setTimeLimitExceededAt(@UptimeMillisLong long timeLimitExceededAt) {
+ public void setTimeLimitExceededAt(@ElapsedRealtimeLong long timeLimitExceededAt) {
mTimeLimitExceededAt = timeLimitExceededAt;
}
- @UptimeMillisLong
+ @ElapsedRealtimeLong
public long getTimeLimitExceededAt() {
return mTimeLimitExceededAt;
}
@@ -1858,8 +1858,8 @@
/**
* Called when a time-limited FGS starts.
*/
- public TimeLimitedFgsInfo createTimeLimitedFgsInfo(long nowUptime) {
- return new TimeLimitedFgsInfo(nowUptime);
+ public TimeLimitedFgsInfo createTimeLimitedFgsInfo(@ElapsedRealtimeLong long nowRealtime) {
+ return new TimeLimitedFgsInfo(nowRealtime);
}
/**
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index b8bfeda..e59de6a 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2773,15 +2773,15 @@
}
code = AppOpsManager.opToSwitch(code);
UidState uidState = getUidStateLocked(uid, false);
- if (uidState != null
- && mAppOpsCheckingService.getUidMode(
- uidState.uid, getPersistentId(virtualDeviceId), code)
- != AppOpsManager.opToDefaultMode(code)) {
- final int rawMode =
- mAppOpsCheckingService.getUidMode(
- uidState.uid, getPersistentId(virtualDeviceId), code);
- return raw ? rawMode : uidState.evalMode(code, rawMode);
+ if (uidState != null) {
+ int rawUidMode = mAppOpsCheckingService.getUidMode(
+ uidState.uid, getPersistentId(virtualDeviceId), code);
+
+ if (rawUidMode != AppOpsManager.opToDefaultMode(code)) {
+ return raw ? rawUidMode : uidState.evalMode(code, rawUidMode);
+ }
}
+
Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false);
if (op == null) {
return AppOpsManager.opToDefaultMode(code);
@@ -3682,26 +3682,24 @@
isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag,
virtualDeviceId, pvr.bypass, false);
final int switchCode = AppOpsManager.opToSwitch(code);
+
+ int rawUidMode;
if (isOpAllowedForUid(uid)) {
// Op is always allowed for the UID, do nothing.
// If there is a non-default per UID policy (we set UID op mode only if
// non-default) it takes over, otherwise use the per package policy.
- } else if (mAppOpsCheckingService.getUidMode(
- uidState.uid, getPersistentId(virtualDeviceId), switchCode)
+ } else if ((rawUidMode =
+ mAppOpsCheckingService.getUidMode(
+ uidState.uid, getPersistentId(virtualDeviceId), switchCode))
!= AppOpsManager.opToDefaultMode(switchCode)) {
- final int uidMode =
- uidState.evalMode(
- code,
- mAppOpsCheckingService.getUidMode(
- uidState.uid,
- getPersistentId(virtualDeviceId),
- switchCode));
+ final int uidMode = uidState.evalMode(code, rawUidMode);
if (!shouldStartForMode(uidMode, startIfModeDefault)) {
if (DEBUG) {
Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code "
+ switchCode + " (" + code + ") uid " + uid + " package "
- + packageName + " flags: " + AppOpsManager.flagsToString(flags));
+ + packageName + " flags: "
+ + AppOpsManager.flagsToString(flags));
}
attributedOp.rejected(uidState.getState(), flags);
scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
@@ -3710,8 +3708,8 @@
return new SyncNotedAppOp(uidMode, code, attributionTag, packageName);
}
} else {
- final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true)
- : op;
+ final Op switchOp =
+ switchCode != code ? getOpLocked(ops, switchCode, uid, true) : op;
final int mode =
switchOp.uidState.evalMode(
switchOp.op,
@@ -3721,9 +3719,12 @@
UserHandle.getUserId(switchOp.uid)));
if (mode != AppOpsManager.MODE_ALLOWED
&& (!startIfModeDefault || mode != MODE_DEFAULT)) {
- if (DEBUG) Slog.d(TAG, "startOperation: reject #" + mode + " for code "
- + switchCode + " (" + code + ") uid " + uid + " package "
- + packageName + " flags: " + AppOpsManager.flagsToString(flags));
+ if (DEBUG) {
+ Slog.d(TAG, "startOperation: reject #" + mode + " for code "
+ + switchCode + " (" + code + ") uid " + uid + " package "
+ + packageName + " flags: "
+ + AppOpsManager.flagsToString(flags));
+ }
attributedOp.rejected(uidState.getState(), flags);
scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag,
virtualDeviceId, flags, mode, startType, attributionFlags,
@@ -3731,6 +3732,7 @@
return new SyncNotedAppOp(mode, code, attributionTag, packageName);
}
}
+
if (DEBUG) Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid
+ " package " + packageName + " restricted: " + isRestricted
+ " flags: " + AppOpsManager.flagsToString(flags));
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index dc0e80c..9b37418 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -19,7 +19,6 @@
import android.hardware.display.BrightnessInfo;
import android.os.Handler;
import android.os.IBinder;
-import android.os.PowerManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.display.brightness.clamper.HdrClamper;
@@ -148,9 +147,7 @@
float getHdrBrightnessValue() {
float hdrBrightness = mHbmController.getHdrBrightnessValue();
- float brightnessMax = mUseHdrClamper ? mHdrClamper.getMaxBrightness()
- : PowerManager.BRIGHTNESS_MAX;
- return Math.min(hdrBrightness, brightnessMax);
+ return mUseHdrClamper ? mHdrClamper.clamp(hdrBrightness) : hdrBrightness;
}
float getTransitionPoint() {
diff --git a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
index f1cb66c..902daa4 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/HdrClamper.java
@@ -59,6 +59,11 @@
private boolean mAutoBrightnessEnabled = false;
+ /**
+ * Indicates that maxBrightness is changed, and we should use slow transition
+ */
+ private boolean mUseSlowTransition = false;
+
public HdrClamper(BrightnessClamperController.ClamperChangeListener clamperChangeListener,
Handler handler) {
this(clamperChangeListener, handler, new Injector());
@@ -72,6 +77,7 @@
mDebouncer = () -> {
mTransitionRate = mDesiredTransitionRate;
mMaxBrightness = mDesiredMaxBrightness;
+ mUseSlowTransition = true;
mClamperChangeListener.onChanged();
};
mHdrListener = injector.getHdrListener((visible) -> {
@@ -80,14 +86,24 @@
}, handler);
}
- // Called in same looper: mHandler.getLooper()
+ /**
+ * Applies clamping
+ * Called in same looper: mHandler.getLooper()
+ */
+ public float clamp(float brightness) {
+ return Math.min(brightness, mMaxBrightness);
+ }
+
+ @VisibleForTesting
public float getMaxBrightness() {
return mMaxBrightness;
}
// Called in same looper: mHandler.getLooper()
public float getTransitionRate() {
- return mTransitionRate;
+ float expectedTransitionRate = mUseSlowTransition ? mTransitionRate : -1;
+ mUseSlowTransition = false;
+ return expectedTransitionRate;
}
/**
@@ -173,7 +189,8 @@
mMaxBrightness = PowerManager.BRIGHTNESS_MAX;
mDesiredMaxBrightness = PowerManager.BRIGHTNESS_MAX;
mDesiredTransitionRate = -1f;
- mTransitionRate = 1f;
+ mTransitionRate = -1f;
+ mUseSlowTransition = false;
mClamperChangeListener.onChanged();
}
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index 0e8a5fb..a818eab 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -249,6 +249,16 @@
mCurrentDream.mAppTask = appTask;
}
+ void setDreamHasFocus(boolean hasFocus) {
+ if (mCurrentDream != null) {
+ mCurrentDream.mDreamHasFocus = hasFocus;
+ }
+ }
+
+ boolean dreamHasFocus() {
+ return mCurrentDream != null && mCurrentDream.mDreamHasFocus;
+ }
+
/**
* Sends a user activity signal to PowerManager to stop the screen from turning off immediately
* if there hasn't been any user interaction in a while.
@@ -271,6 +281,21 @@
stopDreamInstance(immediate, reason, mCurrentDream);
}
+ public boolean bringDreamToFront() {
+ if (mCurrentDream == null || mCurrentDream.mService == null) {
+ return false;
+ }
+
+ try {
+ mCurrentDream.mService.comeToFront();
+ return true;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error asking dream to come to the front", e);
+ }
+
+ return false;
+ }
+
/**
* Stops the given dream instance.
*
@@ -426,6 +451,7 @@
private String mStopReason;
private long mDreamStartTime;
public boolean mWakingGently;
+ public boolean mDreamHasFocus;
private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded;
private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 42c9e08..fc63494 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -20,6 +20,7 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.service.dreams.Flags.dreamTracksFocus;
import static com.android.server.wm.ActivityInterceptorCallback.DREAM_MANAGER_ORDERED_ID;
@@ -406,8 +407,10 @@
/** Whether dreaming can start given user settings and the current dock/charge state. */
private boolean canStartDreamingInternal(boolean isScreenOn) {
synchronized (mLock) {
- // Can't start dreaming if we are already dreaming.
- if (isScreenOn && isDreamingInternal()) {
+ // Can't start dreaming if we are already dreaming and the dream has focus. If we are
+ // dreaming but the dream does not have focus, then the dream can be brought to the
+ // front so it does have focus.
+ if (isScreenOn && isDreamingInternal() && dreamHasFocus()) {
return false;
}
@@ -442,11 +445,20 @@
}
}
+ private boolean dreamHasFocus() {
+ // Dreams always had focus before they were able to track it.
+ return !dreamTracksFocus() || mController.dreamHasFocus();
+ }
+
protected void requestStartDreamFromShell() {
requestDreamInternal();
}
private void requestDreamInternal() {
+ if (isDreamingInternal() && !dreamHasFocus() && mController.bringDreamToFront()) {
+ return;
+ }
+
// Ask the power manager to nap. It will eventually call back into
// startDream() if/when it is appropriate to start dreaming.
// Because napping could cause the screen to turn off immediately if the dream
@@ -1128,6 +1140,16 @@
});
}
+ @Override
+ public void onDreamFocusChanged(boolean hasFocus) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mController.setDreamHasFocus(hasFocus);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
boolean canLaunchDreamActivity(String dreamPackageName, String packageName,
int callingUid) {
if (dreamPackageName == null || packageName == null) {
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index b18871c..a0dbfa0 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -28,6 +28,7 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Random;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -77,9 +78,12 @@
private final AtomicInteger mNextAvailableId = new AtomicInteger();
/**
- * The next available message sequence number
+ * The next available message sequence number. We choose a random
+ * number to start with to avoid collisions and limit the bound to
+ * half of the max value to avoid overflow.
*/
- private final AtomicInteger mNextAvailableMessageSequenceNumber = new AtomicInteger();
+ private final AtomicInteger mNextAvailableMessageSequenceNumber =
+ new AtomicInteger(new Random().nextInt(Integer.MAX_VALUE / 2));
/*
* An executor and the future object for scheduling timeout timers
diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp
index d7dd902..6dadf8f 100644
--- a/services/core/java/com/android/server/power/hint/Android.bp
+++ b/services/core/java/com/android/server/power/hint/Android.bp
@@ -11,3 +11,10 @@
name: "power_hint_flags_lib",
aconfig_declarations: "power_hint_flags",
}
+
+java_aconfig_library {
+ name: "power_hint_flags_lib_host",
+ aconfig_declarations: "power_hint_flags",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index f925b5f..236a746 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -77,6 +77,7 @@
import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
import static android.content.pm.ActivityInfo.FLAG_STATE_NOT_NEEDED;
import static android.content.pm.ActivityInfo.FLAG_TURN_SCREEN_ON;
+import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION;
import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
@@ -2119,9 +2120,19 @@
mCameraCompatControlEnabled = mWmService.mContext.getResources()
.getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled);
mResolveConfigHint = new TaskFragment.ConfigOverrideHint();
- mResolveConfigHint.mUseLegacyInsetsForStableBounds =
- mWmService.mFlags.mInsetsDecoupledConfiguration
- && !info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED);
+ if (mWmService.mFlags.mInsetsDecoupledConfiguration) {
+ // When the stable configuration is the default behavior, override for the legacy apps
+ // without forward override flag.
+ mResolveConfigHint.mUseOverrideInsetsForStableBounds =
+ !info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED)
+ && !info.isChangeEnabled(
+ OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION);
+ } else {
+ // When the stable configuration is not the default behavior, forward overriding the
+ // listed apps.
+ mResolveConfigHint.mUseOverrideInsetsForStableBounds =
+ info.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION);
+ }
mTargetSdk = info.applicationInfo.targetSdkVersion;
@@ -8466,7 +8477,7 @@
mCompatDisplayInsets =
new CompatDisplayInsets(
mDisplayContent, this, letterboxedContainerBounds,
- mResolveConfigHint.mUseLegacyInsetsForStableBounds);
+ mResolveConfigHint.mUseOverrideInsetsForStableBounds);
}
private void clearSizeCompatModeAttributes() {
@@ -8670,7 +8681,7 @@
if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) {
rotation = mDisplayContent.getRotation();
}
- if (!mResolveConfigHint.mUseLegacyInsetsForStableBounds
+ if (!mResolveConfigHint.mUseOverrideInsetsForStableBounds
|| getCompatDisplayInsets() != null
|| isFloating(parentWindowingMode) || parentAppBounds == null
|| parentAppBounds.isEmpty() || rotation == ROTATION_UNDEFINED) {
@@ -8987,7 +8998,7 @@
if (mDisplayContent == null) {
return true;
}
- if (!mResolveConfigHint.mUseLegacyInsetsForStableBounds) {
+ if (!mResolveConfigHint.mUseOverrideInsetsForStableBounds) {
// No insets should be considered any more.
return true;
}
@@ -9006,7 +9017,7 @@
final Task task = getTask();
task.calculateInsetFrames(outNonDecorBounds /* outNonDecorBounds */,
outStableBounds /* outStableBounds */, parentBounds /* bounds */, di,
- mResolveConfigHint.mUseLegacyInsetsForStableBounds);
+ mResolveConfigHint.mUseOverrideInsetsForStableBounds);
final int orientationWithInsets = outStableBounds.height() >= outStableBounds.width()
? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE;
// If orientation does not match the orientation with insets applied, then a
@@ -9063,7 +9074,7 @@
getResolvedOverrideConfiguration().windowConfiguration.getBounds();
final int stableBoundsOrientation = stableBounds.width() > stableBounds.height()
? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
- final int parentOrientation = mResolveConfigHint.mUseLegacyInsetsForStableBounds
+ final int parentOrientation = mResolveConfigHint.mUseOverrideInsetsForStableBounds
? stableBoundsOrientation : newParentConfig.orientation;
// If the activity requires a different orientation (either by override or activityInfo),
@@ -9088,7 +9099,7 @@
return;
}
- final Rect parentAppBounds = mResolveConfigHint.mUseLegacyInsetsForStableBounds
+ final Rect parentAppBounds = mResolveConfigHint.mUseOverrideInsetsForStableBounds
? outNonDecorBounds : newParentConfig.windowConfiguration.getAppBounds();
// TODO(b/182268157): Explore using only one type of parentBoundsWithInsets, either app
// bounds or stable bounds to unify aspect ratio logic.
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index a739e57..b3c43bc 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3736,6 +3736,14 @@
return false;
}
+ // If the app is using auto-enter, and it explicitly requests entering PiP while pausing,
+ // return false immediately since auto-enter should take in place instead.
+ if (fromClient && r.isState(PAUSING) && params.isAutoEnterEnabled()) {
+ Slog.w(TAG, "Skip client enterPictureInPictureMode request while pausing,"
+ + " auto-enter-pip is enabled");
+ return false;
+ }
+
if (isPip2ExperimentEnabled()) {
// If PiP2 flag is on and request to enter PiP comes in,
// we request a direct transition TRANSIT_PIP from Shell to get the right entry bounds.
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index cb5ad91..da1581e 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -905,13 +905,11 @@
// Note that we check the task rather than the parent as with ActivityEmbedding the parent might
// be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is
// actually fullscreen.
- private boolean isDisplayFullScreenAndInPosture(DeviceStateController.DeviceState state,
- boolean isTabletop) {
+ private boolean isDisplayFullScreenAndInPosture(boolean isTabletop) {
Task task = mActivityRecord.getTask();
- return mActivityRecord.mDisplayContent != null
- && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(state,
- isTabletop)
- && task != null
+ return mActivityRecord.mDisplayContent != null && task != null
+ && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(
+ DeviceStateController.DeviceState.HALF_FOLDED, isTabletop)
&& task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
}
@@ -939,16 +937,14 @@
}
private boolean isFullScreenAndBookModeEnabled() {
- return isDisplayFullScreenAndInPosture(
- DeviceStateController.DeviceState.HALF_FOLDED, false /* isTabletop */)
+ return isDisplayFullScreenAndInPosture(/* isTabletop */ false)
&& mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled();
}
float getVerticalPositionMultiplier(Configuration parentConfiguration) {
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
- boolean tabletopMode = isDisplayFullScreenAndInPosture(
- DeviceStateController.DeviceState.HALF_FOLDED, true /* isTabletop */);
+ boolean tabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true);
return isVerticalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
@@ -981,16 +977,15 @@
}
private boolean shouldUseSplitScreenAspectRatio(@NonNull Configuration parentConfiguration) {
- final boolean isBookMode = isDisplayFullScreenAndInPosture(
- DeviceStateController.DeviceState.HALF_FOLDED,
- /* isTabletop */ false);
+ final boolean isBookMode = isDisplayFullScreenAndInPosture(/* isTabletop */ false);
final boolean isNotCenteredHorizontally = getHorizontalPositionMultiplier(
parentConfiguration) != LETTERBOX_POSITION_MULTIPLIER_CENTER;
- final boolean isTabletopMode = isDisplayFullScreenAndInPosture(
- DeviceStateController.DeviceState.HALF_FOLDED,
- /* isTabletop */ true);
+ final boolean isTabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true);
+ final boolean isLandscape = isFixedOrientationLandscape(
+ mActivityRecord.getOverrideOrientation());
+
// Don't resize to split screen size when in book mode if letterbox position is centered
- return ((isBookMode && isNotCenteredHorizontally) || isTabletopMode)
+ return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape)
|| isCameraCompatSplitScreenAspectRatioAllowed()
&& isCameraCompatTreatmentActive();
}
@@ -1647,17 +1642,13 @@
if (isHorizontalReachabilityEnabled()) {
int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
.getLetterboxPositionForHorizontalReachability(
- isDisplayFullScreenAndInPosture(
- DeviceStateController.DeviceState.HALF_FOLDED,
- false /* isTabletop */));
+ isDisplayFullScreenAndInPosture(/* isTabletop */ false));
positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
letterboxPositionForHorizontalReachability);
} else if (isVerticalReachabilityEnabled()) {
int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
.getLetterboxPositionForVerticalReachability(
- isDisplayFullScreenAndInPosture(
- DeviceStateController.DeviceState.HALF_FOLDED,
- true /* isTabletop */));
+ isDisplayFullScreenAndInPosture(/* isTabletop */ true));
positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
letterboxPositionForVerticalReachability);
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 7a8bea0..2b631f7 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2204,7 +2204,7 @@
static class ConfigOverrideHint {
@Nullable DisplayInfo mTmpOverrideDisplayInfo;
@Nullable ActivityRecord.CompatDisplayInsets mTmpCompatInsets;
- boolean mUseLegacyInsetsForStableBounds;
+ boolean mUseOverrideInsetsForStableBounds;
}
void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
@@ -2237,11 +2237,11 @@
@NonNull Configuration parentConfig, @Nullable ConfigOverrideHint overrideHint) {
DisplayInfo overrideDisplayInfo = null;
ActivityRecord.CompatDisplayInsets compatInsets = null;
- boolean useLegacyInsetsForStableBounds = false;
+ boolean useOverrideInsetsForStableBounds = false;
if (overrideHint != null) {
overrideDisplayInfo = overrideHint.mTmpOverrideDisplayInfo;
compatInsets = overrideHint.mTmpCompatInsets;
- useLegacyInsetsForStableBounds = overrideHint.mUseLegacyInsetsForStableBounds;
+ useOverrideInsetsForStableBounds = overrideHint.mUseOverrideInsetsForStableBounds;
if (overrideDisplayInfo != null) {
// Make sure the screen related configs can be computed by the provided
// display info.
@@ -2321,7 +2321,7 @@
// The non decor inset are areas that could never be removed in Honeycomb. See
// {@link WindowManagerPolicy#getNonDecorInsetsLw}.
calculateInsetFrames(mTmpNonDecorBounds, mTmpStableBounds, mTmpFullBounds, di,
- useLegacyInsetsForStableBounds);
+ useOverrideInsetsForStableBounds);
} else {
// Apply the given non-decor and stable insets to calculate the corresponding bounds
// for screen size of configuration.
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 407b5d5..eeedec3 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -1044,6 +1044,13 @@
int[] fromOrientations, int[] toOrientations);
/**
+ * Set current screen capture session id that will be used during sensitive content protections.
+ *
+ * @param sessionId Session id for this screen capture protection
+ */
+ public abstract void setBlockScreenCaptureForAppsSessionId(long sessionId);
+
+ /**
* Set whether screen capture should be disabled for all windows of a specific app windows based
* on sensitive content protections.
*
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 07e534c..de7c0eb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -44,6 +44,7 @@
import static android.os.Process.myUid;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.permission.flags.Flags.sensitiveContentImprovements;
+import static android.permission.flags.Flags.sensitiveContentMetricsBugfix;
import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW;
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
@@ -113,6 +114,7 @@
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_MOVEMENT;
import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR;
import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED;
import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN;
import static com.android.server.LockGuard.INDEX_WINDOW;
import static com.android.server.LockGuard.installLock;
@@ -340,6 +342,7 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastPrintWriter;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.LatencyTracker;
import com.android.internal.view.WindowManagerPolicyThread;
import com.android.server.AnimationThread;
@@ -1110,6 +1113,9 @@
SystemPerformanceHinter mSystemPerformanceHinter;
@GuardedBy("mGlobalLock")
+ private long mSensitiveContentProtectionSessionId = 0;
+
+ @GuardedBy("mGlobalLock")
final SensitiveContentPackages mSensitiveContentPackages = new SensitiveContentPackages();
/**
* UIDs for which a Toast has been shown to indicate
@@ -4519,7 +4525,8 @@
}
}
- int getDisplayUserRotation(int displayId) {
+ @Override
+ public int getDisplayUserRotation(int displayId) {
synchronized (mGlobalLock) {
final DisplayContent display = mRoot.getDisplayContent(displayId);
if (display == null) {
@@ -8739,6 +8746,16 @@
}
@Override
+ public void setBlockScreenCaptureForAppsSessionId(long sessionId) {
+ synchronized (mGlobalLock) {
+ if (sensitiveContentMetricsBugfix()
+ && mSensitiveContentProtectionSessionId != sessionId) {
+ mSensitiveContentProtectionSessionId = sessionId;
+ }
+ }
+ }
+
+ @Override
public void addBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) {
synchronized (mGlobalLock) {
boolean modified =
@@ -10202,6 +10219,15 @@
Toast.LENGTH_SHORT)
.show();
});
+ // If blocked due to notification protection (null window token) log protection applied
+ if (sensitiveContentMetricsBugfix()
+ && mSensitiveContentPackages
+ .shouldBlockScreenCaptureForApp(w.getOwningPackage(), uid, null)) {
+ FrameworkStatsLog.write(
+ SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED,
+ mSensitiveContentProtectionSessionId,
+ uid);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 9d8246d..d91a211 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -631,6 +631,11 @@
return;
}
+ final WindowProcessController caller = mAtm.mProcessMap.getProcess(r.launchedFromPid);
+ if (caller != null && caller.mInstrumenting) {
+ return;
+ }
+
final long diff = launchTime - lastLaunchTime;
if (diff < RAPID_ACTIVITY_LAUNCH_MS) {
mRapidActivityLaunchCount++;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
index c4e2dc8..b76279b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java
@@ -32,7 +32,6 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
* Implementation of {@link DevicePolicyCache}, to which {@link DevicePolicyManagerService} pushes
@@ -48,18 +47,11 @@
private final Object mLock = new Object();
/**
- * Indicates which user is screen capture disallowed on. Can be {@link UserHandle#USER_NULL},
- * {@link UserHandle#USER_ALL} or a concrete user ID.
- */
- @GuardedBy("mLock")
- private int mScreenCaptureDisallowedUser = UserHandle.USER_NULL;
-
- /**
* Indicates if screen capture is disallowed on a specific user or all users if
* it contains {@link UserHandle#USER_ALL}.
*/
@GuardedBy("mLock")
- private Set<Integer> mScreenCaptureDisallowedUsers = new HashSet<>();
+ private final Set<Integer> mScreenCaptureDisallowedUsers = new HashSet<>();
@GuardedBy("mLock")
private final SparseIntArray mPasswordQuality = new SparseIntArray();
@@ -70,9 +62,8 @@
@GuardedBy("mLock")
private ArrayMap<String, String> mLauncherShortcutOverrides = new ArrayMap<>();
-
/** Maps to {@code ActiveAdmin.mAdminCanGrantSensorsPermissions}. */
- private final AtomicBoolean mCanGrantSensorsPermissions = new AtomicBoolean(false);
+ private volatile boolean mCanGrantSensorsPermissions = false;
@GuardedBy("mLock")
private final SparseIntArray mContentProtectionPolicy = new SparseIntArray();
@@ -87,10 +78,6 @@
@Override
public boolean isScreenCaptureAllowed(int userHandle) {
- return isScreenCaptureAllowedInPolicyEngine(userHandle);
- }
-
- private boolean isScreenCaptureAllowedInPolicyEngine(int userHandle) {
// This won't work if resolution mechanism is not strictest applies, but it's ok for now.
synchronized (mLock) {
return !mScreenCaptureDisallowedUsers.contains(userHandle)
@@ -98,18 +85,6 @@
}
}
- public int getScreenCaptureDisallowedUser() {
- synchronized (mLock) {
- return mScreenCaptureDisallowedUser;
- }
- }
-
- public void setScreenCaptureDisallowedUser(int userHandle) {
- synchronized (mLock) {
- mScreenCaptureDisallowedUser = userHandle;
- }
- }
-
public void setScreenCaptureDisallowedUser(int userHandle, boolean disallowed) {
synchronized (mLock) {
if (disallowed) {
@@ -170,12 +145,12 @@
@Override
public boolean canAdminGrantSensorsPermissions() {
- return mCanGrantSensorsPermissions.get();
+ return mCanGrantSensorsPermissions;
}
/** Sets admin control over permission grants. */
public void setAdminCanGrantSensorsPermissions(boolean canGrant) {
- mCanGrantSensorsPermissions.set(canGrant);
+ mCanGrantSensorsPermissions = canGrant;
}
@Override
@@ -205,7 +180,7 @@
pw.println("Password quality: " + mPasswordQuality);
pw.println("Permission policy: " + mPermissionPolicy);
pw.println("Content protection policy: " + mContentProtectionPolicy);
- pw.println("Admin can grant sensors permission: " + mCanGrantSensorsPermissions.get());
+ pw.println("Admin can grant sensors permission: " + mCanGrantSensorsPermissions);
pw.print("Shortcuts overrides: ");
pw.println(mLauncherShortcutOverrides);
pw.decreaseIndent();
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 7761311..6458eac 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -12320,7 +12320,8 @@
if (Flags.headlessDeviceOwnerSingleUserEnabled()) {
// Block this method if the device is in headless main user mode
Preconditions.checkCallAuthorization(
- getHeadlessDeviceOwnerModeForDeviceOwner()
+ !mInjector.userManagerIsHeadlessSystemUserMode()
+ || getHeadlessDeviceOwnerModeForDeviceOwner()
!= HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER,
"createAndManageUser was called while in headless single user mode");
}
@@ -23319,10 +23320,12 @@
}
private boolean hasAdminPolicy(int adminPolicy, String callerPackageName) {
- CallerIdentity caller = getCallerIdentity(callerPackageName);
- ActiveAdmin deviceAdmin = getActiveAdminWithPolicyForUidLocked(
- null, adminPolicy, caller.getUid());
- return deviceAdmin != null;
+ synchronized (getLockObject()) {
+ CallerIdentity caller = getCallerIdentity(callerPackageName);
+ ActiveAdmin deviceAdmin = getActiveAdminWithPolicyForUidLocked(
+ null, adminPolicy, caller.getUid());
+ return deviceAdmin != null;
+ }
}
/**
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
index 16daacb..aa7532a 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
@@ -412,7 +412,7 @@
/* stickyKeepInnerUntil45Degrees */ true,
PreferredScreen.INNER,
/* setStickyKeepOuterUntil90Degrees */ null,
- /* setStickyKeepInnerUntil45Degrees */ false
+ /* setStickyKeepInnerUntil45Degrees */ null
));
DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
HingeAngle.ANGLE_45_TO_90,
@@ -492,7 +492,7 @@
/* stickyKeepInnerUntil45Degrees */ true,
PreferredScreen.INNER,
/* setStickyKeepOuterUntil90Degrees */ null,
- /* setStickyKeepInnerUntil45Degrees */ false
+ /* setStickyKeepInnerUntil45Degrees */ null
));
DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
HingeAngle.ANGLE_45_TO_90,
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
index 9f07aa8..2d725d1 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
@@ -511,7 +511,7 @@
}
@Test
- public void test_unfoldTo60Degrees_andFoldTo10_switchesToClosedState() {
+ public void test_unfoldTo60Degrees_andFoldTo10_doesNotSwitchToClosedState() {
sendHingeAngle(0f);
sendRightSideFlatSensorEvent(false);
mProvider.setListener(mListener);
@@ -522,6 +522,36 @@
sendHingeAngle(10f);
+ verify(mListener, never()).onStateChanged(anyInt());
+ }
+
+ @Test
+ public void test_unfoldTo100Degrees_andFoldTo10_switchesToClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ sendHingeAngle(100f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(10f);
+
+ verify(mListener).onStateChanged(DEVICE_STATE_CLOSED);
+ }
+
+ @Test
+ public void test_unfoldTo10Degrees_andFoldTo0_switchesToClosedState() {
+ sendHingeAngle(0f);
+ sendRightSideFlatSensorEvent(false);
+ mProvider.setListener(mListener);
+ assertLatestReportedState(DEVICE_STATE_CLOSED);
+ sendHingeAngle(10f);
+ assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+ clearInvocations(mListener);
+
+ sendHingeAngle(0f);
+
verify(mListener).onStateChanged(DEVICE_STATE_CLOSED);
}
diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
index c0d988d..b0c7073 100644
--- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
@@ -20,6 +20,7 @@
import android.companion.virtual.VirtualDeviceManager
import android.os.Handler
import android.os.UserHandle
+import android.permission.PermissionManager
import android.permission.flags.Flags
import android.util.ArrayMap
import android.util.ArraySet
@@ -142,7 +143,7 @@
}
}
- override fun getNonDefaultUidModes(uid: Int, persistentDeviceId: String): SparseIntArray {
+ override fun getNonDefaultUidModes(uid: Int, deviceId: String): SparseIntArray {
val appId = UserHandle.getAppId(uid)
val userId = UserHandle.getUserId(uid)
service.getState {
@@ -150,7 +151,8 @@
with(appIdPolicy) { opNameMapToOpSparseArray(getAppOpModes(appId, userId)?.map) }
if (Flags.runtimePermissionAppopsMappingEnabled()) {
runtimePermissionNameToAppOp.forEachIndexed { _, permissionName, appOpCode ->
- val mode = getUidModeFromPermissionState(appId, userId, permissionName)
+ val mode =
+ getUidModeFromPermissionState(appId, userId, permissionName, deviceId)
if (mode != AppOpsManager.opToDefaultMode(appOpCode)) {
modes[appOpCode] = mode
}
@@ -165,7 +167,7 @@
return opNameMapToOpSparseArray(getPackageModes(packageName, userId))
}
- override fun getUidMode(uid: Int, persistentDeviceId: String, op: Int): Int {
+ override fun getUidMode(uid: Int, deviceId: String, op: Int): Int {
val appId = UserHandle.getAppId(uid)
val userId = UserHandle.getUserId(uid)
val opName = AppOpsManager.opToPublicName(op)
@@ -174,7 +176,9 @@
return if (!Flags.runtimePermissionAppopsMappingEnabled() || permissionName == null) {
service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } }
} else {
- service.getState { getUidModeFromPermissionState(appId, userId, permissionName) }
+ service.getState {
+ getUidModeFromPermissionState(appId, userId, permissionName, deviceId)
+ }
}
}
@@ -187,15 +191,31 @@
private fun GetStateScope.getUidModeFromPermissionState(
appId: Int,
userId: Int,
- permissionName: String
+ permissionName: String,
+ deviceId: String
): Int {
+ val checkDevicePermissionFlags =
+ deviceId != VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT &&
+ permissionName in PermissionManager.DEVICE_AWARE_PERMISSIONS
val permissionFlags =
- with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) }
+ if (checkDevicePermissionFlags) {
+ with(devicePermissionPolicy) {
+ getPermissionFlags(appId, deviceId, userId, permissionName)
+ }
+ } else {
+ with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) }
+ }
val backgroundPermissionName = foregroundToBackgroundPermissionName[permissionName]
val backgroundPermissionFlags =
if (backgroundPermissionName != null) {
- with(permissionPolicy) {
- getPermissionFlags(appId, userId, backgroundPermissionName)
+ if (checkDevicePermissionFlags) {
+ with(devicePermissionPolicy) {
+ getPermissionFlags(appId, deviceId, userId, backgroundPermissionName)
+ }
+ } else {
+ with(permissionPolicy) {
+ getPermissionFlags(appId, userId, backgroundPermissionName)
+ }
}
} else {
PermissionFlags.RUNTIME_GRANTED
@@ -207,7 +227,7 @@
val fullerPermissionName =
PermissionService.getFullerPermission(permissionName) ?: return result
- return getUidModeFromPermissionState(appId, userId, fullerPermissionName)
+ return getUidModeFromPermissionState(appId, userId, fullerPermissionName, deviceId)
}
private fun evaluateModeFromPermissionFlags(
@@ -224,7 +244,7 @@
MODE_IGNORED
}
- override fun setUidMode(uid: Int, persistentDeviceId: String, code: Int, mode: Int): Boolean {
+ override fun setUidMode(uid: Int, deviceId: String, code: Int, mode: Int): Boolean {
if (
Flags.runtimePermissionAppopsMappingEnabled() && code in runtimeAppOpToPermissionNames
) {
@@ -308,7 +328,7 @@
// and we have our own persistence.
}
- override fun getForegroundOps(uid: Int, persistentDeviceId: String): SparseBooleanArray {
+ override fun getForegroundOps(uid: Int, deviceId: String): SparseBooleanArray {
return SparseBooleanArray().apply {
getUidModes(uid)?.forEachIndexed { _, op, mode ->
if (mode == AppOpsManager.MODE_FOREGROUND) {
@@ -317,7 +337,7 @@
}
if (Flags.runtimePermissionAppopsMappingEnabled()) {
foregroundableOps.forEachIndexed { _, op, _ ->
- if (getUidMode(uid, persistentDeviceId, op) == AppOpsManager.MODE_FOREGROUND) {
+ if (getUidMode(uid, deviceId, op) == AppOpsManager.MODE_FOREGROUND) {
this[op] = true
}
}
@@ -501,7 +521,7 @@
)
}
}
- ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode ->
+ ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode ->
addPendingChangedModeIfNeeded(
appId,
userId,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index de93914..78ebdb1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1312,7 +1312,7 @@
when(mHolder.hbmController.getHighBrightnessMode()).thenReturn(
BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR);
when(mHolder.hbmController.getHdrBrightnessValue()).thenReturn(PowerManager.BRIGHTNESS_MAX);
- when(mHolder.hdrClamper.getMaxBrightness()).thenReturn(clampedBrightness);
+ when(mHolder.hdrClamper.clamp(PowerManager.BRIGHTNESS_MAX)).thenReturn(clampedBrightness);
when(mHolder.hdrClamper.getTransitionRate()).thenReturn(transitionRate);
mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
index 39ffe5b..c785ea6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/HdrClamperTest.java
@@ -221,6 +221,18 @@
assertEquals(0.04f, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
}
+ @Test
+ public void testCalmper_transitionRateOverriddenByOtherRequest() {
+ mHdrClamper.onAmbientLuxChange(499);
+
+ mClock.fastForward(3000);
+ mTestHandler.timeAdvance();
+ assertEquals(0.6f, mHdrClamper.getMaxBrightness(), FLOAT_TOLERANCE);
+ assertEquals(0.04, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
+ // getTransitionRate should reset transitionRate
+ assertEquals(-1f, mHdrClamper.getTransitionRate(), FLOAT_TOLERANCE);
+ }
+
// MsgInfo.sendTime is calculated first by adding SystemClock.uptimeMillis()
// (in Handler.sendMessageDelayed) and then by subtracting SystemClock.uptimeMillis()
// (in TestHandler.sendMessageAtTime, there might be several milliseconds difference between
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java
new file mode 100644
index 0000000..99968d5
--- /dev/null
+++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.dreams;
+
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.service.dreams.utils.DreamAccessibility;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamAccessibilityTest {
+
+ @Mock
+ private View mView;
+
+ @Mock
+ private Context mContext;
+
+ @Mock
+ private Resources mResources;
+
+ @Mock
+ private AccessibilityNodeInfo mAccessibilityNodeInfo;
+
+ @Captor
+ private ArgumentCaptor<View.AccessibilityDelegate> mAccessibilityDelegateArgumentCaptor;
+
+ private DreamAccessibility mDreamAccessibility;
+ private static final String CUSTOM_ACTION = "Custom Action";
+ private static final String EXISTING_ACTION = "Existing Action";
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mDreamAccessibility = new DreamAccessibility(mContext, mView);
+
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getString(R.string.dream_accessibility_action_click))
+ .thenReturn(CUSTOM_ACTION);
+ }
+ /**
+ * Test to verify the configuration of accessibility actions within a view delegate.
+ */
+ @Test
+ public void testConfigureAccessibilityActions() {
+ when(mAccessibilityNodeInfo.getActionList()).thenReturn(new ArrayList<>());
+
+ mDreamAccessibility.updateAccessibilityConfiguration(false);
+
+ verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
+ View.AccessibilityDelegate capturedDelegate =
+ mAccessibilityDelegateArgumentCaptor.getValue();
+
+ capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo);
+
+ verify(mAccessibilityNodeInfo).addAction(argThat(action ->
+ action.getId() == AccessibilityNodeInfo.ACTION_CLICK
+ && TextUtils.equals(action.getLabel(), CUSTOM_ACTION)));
+ }
+
+ /**
+ * Test to verify the configuration of accessibility actions within a view delegate,
+ * specifically checking the removal of an existing click action and addition
+ * of a new custom action.
+ */
+ @Test
+ public void testConfigureAccessibilityActions_RemovesExistingClickAction() {
+ AccessibilityNodeInfo.AccessibilityAction existingAction =
+ new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
+ EXISTING_ACTION);
+ when(mAccessibilityNodeInfo.getActionList())
+ .thenReturn(Collections.singletonList(existingAction));
+
+ mDreamAccessibility.updateAccessibilityConfiguration(false);
+
+ verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
+ View.AccessibilityDelegate capturedDelegate =
+ mAccessibilityDelegateArgumentCaptor.getValue();
+
+ capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo);
+
+ verify(mAccessibilityNodeInfo).removeAction(existingAction);
+ verify(mAccessibilityNodeInfo).addAction(argThat(action ->
+ action.getId() == AccessibilityNodeInfo.ACTION_CLICK
+ && TextUtils.equals(action.getLabel(), CUSTOM_ACTION)));
+
+ }
+
+ /**
+ * Test to verify the removal of a custom accessibility action within a view delegate.
+ */
+ @Test
+ public void testRemoveCustomAccessibilityAction() {
+
+ AccessibilityNodeInfo.AccessibilityAction existingAction =
+ new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
+ EXISTING_ACTION);
+ when(mAccessibilityNodeInfo.getActionList())
+ .thenReturn(Collections.singletonList(existingAction));
+
+ mDreamAccessibility.updateAccessibilityConfiguration(false);
+ verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture());
+ View.AccessibilityDelegate capturedDelegate =
+ mAccessibilityDelegateArgumentCaptor.getValue();
+ when(mView.getAccessibilityDelegate()).thenReturn(capturedDelegate);
+ clearInvocations(mView);
+
+ mDreamAccessibility.updateAccessibilityConfiguration(true);
+ verify(mView).setAccessibilityDelegate(null);
+ }
+
+ /**
+ * Test to verify the removal of custom accessibility action is not called if delegate is not
+ * set by the dreamService.
+ */
+ @Test
+ public void testRemoveCustomAccessibility_DoesNotRemoveDelegateNotSetByDreamAccessibility() {
+ mDreamAccessibility.updateAccessibilityConfiguration(true);
+ verify(mView, never()).setAccessibilityDelegate(any());
+ }
+}
+
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java
index db70434..88ab871 100644
--- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java
+++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java
@@ -19,6 +19,8 @@
import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER;
import static android.os.PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
@@ -270,6 +272,31 @@
eq(USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS));
}
+ @Test
+ public void setDreamHasFocus_true_dreamHasFocus() {
+ mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+ 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+
+ mDreamController.setDreamHasFocus(true);
+ assertTrue(mDreamController.dreamHasFocus());
+ }
+
+ @Test
+ public void setDreamHasFocus_false_dreamDoesNotHaveFocus() {
+ mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+ 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+
+ mDreamController.setDreamHasFocus(false);
+ assertFalse(mDreamController.dreamHasFocus());
+ }
+
+ @Test
+ public void setDreamHasFocus_notDreaming_dreamDoesNotHaveFocus() {
+ mDreamController.setDreamHasFocus(true);
+ // Dream still doesn't have focus because it was never started.
+ assertFalse(mDreamController.dreamHasFocus());
+ }
+
private ServiceConnection captureServiceConnection() {
verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(),
any());
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
index 124ae20..a20d935 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
@@ -16,15 +16,18 @@
package com.android.server;
+import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX;
import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -34,6 +37,7 @@
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
import android.os.Process;
+import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -309,6 +313,26 @@
}
@Test
+ @RequiresFlagsDisabled(FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX)
+ public void mediaProjectionOnStart_flagDisabled_neverSetBlockScreenCaptureForAppsSessionId() {
+ setupSensitiveNotification();
+
+ mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
+
+ verify(mWindowManager, never()).setBlockScreenCaptureForAppsSessionId(anyLong());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX)
+ public void mediaProjectionOnStart_setBlockScreenCaptureForAppsSessionId() {
+ setupSensitiveNotification();
+
+ mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
+
+ verify(mWindowManager).setBlockScreenCaptureForAppsSessionId(anyLong());
+ }
+
+ @Test
public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() {
ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification();
@@ -323,7 +347,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -332,7 +356,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -400,7 +424,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -411,7 +435,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -422,7 +446,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -435,7 +459,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
index b487dc6..c970a3e 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
@@ -16,7 +16,8 @@
package com.android.server.appop;
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
import static com.google.common.truth.Truth.assertThat;
@@ -31,6 +32,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
+import android.Manifest;
import android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpActiveChangedListener;
import android.companion.virtual.VirtualDeviceManager;
@@ -38,7 +40,8 @@
import android.content.AttributionSource;
import android.content.Context;
import android.os.Process;
-import android.virtualdevice.cts.common.FakeAssociationRule;
+import android.permission.PermissionManager;
+import android.virtualdevice.cts.common.VirtualDeviceRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -49,7 +52,6 @@
import org.junit.runner.RunWith;
import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
/**
* Tests app ops version upgrades
@@ -59,7 +61,13 @@
public class AppOpsActiveWatcherTest {
@Rule
- public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
+ public VirtualDeviceRule virtualDeviceRule =
+ VirtualDeviceRule.withAdditionalPermissions(
+ Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+ Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
+ Manifest.permission.CREATE_VIRTUAL_DEVICE,
+ Manifest.permission.GET_APP_OPS_STATS
+ );
private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
@Test
@@ -145,20 +153,28 @@
@Test
public void testWatchActiveOpsForExternalDevice() {
- final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
- VirtualDeviceManager.class);
- AtomicInteger virtualDeviceId = new AtomicInteger();
- runWithShellPermissionIdentity(() -> {
- final VirtualDeviceManager.VirtualDevice virtualDevice =
- virtualDeviceManager.createVirtualDevice(
- mFakeAssociationRule.getAssociationInfo().getId(),
- new VirtualDeviceParams.Builder().setName("virtual_device").build());
- virtualDeviceId.set(virtualDevice.getDeviceId());
- });
+ VirtualDeviceManager.VirtualDevice virtualDevice =
+ virtualDeviceRule.createManagedVirtualDevice(
+ new VirtualDeviceParams.Builder()
+ .setDevicePolicy(POLICY_TYPE_CAMERA, DEVICE_POLICY_CUSTOM)
+ .build()
+ );
+
+ PermissionManager permissionManager =
+ getContext().getSystemService(PermissionManager.class);
+
+ // Unlike runtime permission being automatically granted to the default device, we need to
+ // grant camera permission to the external device first before we can start op.
+ permissionManager.grantRuntimePermission(
+ getContext().getOpPackageName(),
+ Manifest.permission.CAMERA,
+ virtualDevice.getPersistentDeviceId()
+ );
+
final OnOpActiveChangedListener listener = mock(OnOpActiveChangedListener.class);
AttributionSource attributionSource = new AttributionSource(Process.myUid(),
getContext().getOpPackageName(), getContext().getAttributionTag(),
- virtualDeviceId.get());
+ virtualDevice.getDeviceId());
final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
appOpsManager.startWatchingActive(new String[]{AppOpsManager.OPSTR_CAMERA,
@@ -171,7 +187,7 @@
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
eq(Process.myUid()), eq(getContext().getOpPackageName()),
- eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(true),
+ eq(getContext().getAttributionTag()), eq(virtualDevice.getDeviceId()), eq(true),
eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
verifyNoMoreInteractions(listener);
@@ -182,7 +198,7 @@
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA),
eq(Process.myUid()), eq(getContext().getOpPackageName()),
- eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(false),
+ eq(getContext().getAttributionTag()), eq(virtualDevice.getDeviceId()), eq(false),
eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE));
verifyNoMoreInteractions(listener);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index 400f9bb..4f94704 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -317,6 +317,70 @@
}
@Test
+ public void testEnterPipModeWhenResumed_autoEnterEnabled_returnTrue() {
+ final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+ final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+ PictureInPictureParams params = mock(PictureInPictureParams.class);
+ activity.pictureInPictureArgs = params;
+
+ doReturn(true).when(activity).isState(RESUMED);
+ doReturn(false).when(activity).inPinnedWindowingMode();
+ doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+ doReturn(true).when(params).isAutoEnterEnabled();
+
+ assertTrue(mAtm.enterPictureInPictureMode(activity, params,
+ true /* fromClient */, true /* isAutoEnter */));
+ }
+
+ @Test
+ public void testEnterPipModeWhenResumed_autoEnterDisabled_returnTrue() {
+ final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+ final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+ PictureInPictureParams params = mock(PictureInPictureParams.class);
+ activity.pictureInPictureArgs = params;
+
+ doReturn(true).when(activity).isState(RESUMED);
+ doReturn(false).when(activity).inPinnedWindowingMode();
+ doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+ doReturn(false).when(params).isAutoEnterEnabled();
+
+ assertTrue(mAtm.enterPictureInPictureMode(activity, params,
+ true /* fromClient */, false /* isAutoEnter */));
+ }
+
+ @Test
+ public void testEnterPipModeWhenPausing_autoEnterEnabled_returnFalse() {
+ final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+ final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+ PictureInPictureParams params = mock(PictureInPictureParams.class);
+ activity.pictureInPictureArgs = params;
+
+ doReturn(true).when(activity).isState(PAUSING);
+ doReturn(false).when(activity).inPinnedWindowingMode();
+ doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+ doReturn(true).when(params).isAutoEnterEnabled();
+
+ assertFalse(mAtm.enterPictureInPictureMode(activity, params,
+ true /* fromClient */, true /* isAutoEnter */));
+ }
+
+ @Test
+ public void testEnterPipModeWhenPausing_autoEnterDisabled_returnTrue() {
+ final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+ final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
+ PictureInPictureParams params = mock(PictureInPictureParams.class);
+ activity.pictureInPictureArgs = params;
+
+ doReturn(true).when(activity).isState(PAUSING);
+ doReturn(false).when(activity).inPinnedWindowingMode();
+ doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean());
+ doReturn(false).when(params).isAutoEnterEnabled();
+
+ assertTrue(mAtm.enterPictureInPictureMode(activity, params,
+ true /* fromClient */, false /* isAutoEnter */));
+ }
+
+ @Test
public void testResumeNextActivityOnCrashedAppDied() {
mSupervisor.beginDeferResume();
final ActivityRecord homeActivity = new ActivityBuilder(mAtm)
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 000162a..ac1dc08 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4484,6 +4484,21 @@
}
@Test
+ public void testPortraitAppInTabletop_notSplitScreen() {
+ final int dw = 2400;
+ setUpDisplaySizeWithApp(dw, 2000);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+ final int initialWidth = mActivity.getBounds().width();
+
+ setFoldablePosture(true /* isHalfFolded */, true /* isTabletop */);
+
+ final int finalWidth = mActivity.getBounds().width();
+ assertEquals(initialWidth, finalWidth);
+ assertNotEquals(finalWidth, getExpectedSplitSize(dw));
+ }
+
+ @Test
public void testUpdateResolvedBoundsHorizontalPosition_bookModeEnabled() {
// Set up a display in landscape with a fixed-orientation PORTRAIT app
setUpDisplaySizeWithApp(2800, 1400);
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 87bb0f0..b005715 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -954,6 +954,11 @@
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
public static final int SATELLITE_MODEM_STATE_CONNECTED = 7;
/**
+ * The satellite modem is being powered on.
+ * @hide
+ */
+ public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8;
+ /**
* Satellite modem state is unknown. This generic modem state should be used only when the
* modem state cannot be mapped to other specific modem states.
*/
@@ -970,6 +975,7 @@
SATELLITE_MODEM_STATE_UNAVAILABLE,
SATELLITE_MODEM_STATE_NOT_CONNECTED,
SATELLITE_MODEM_STATE_CONNECTED,
+ SATELLITE_MODEM_STATE_ENABLING_SATELLITE,
SATELLITE_MODEM_STATE_UNKNOWN
})
@Retention(RetentionPolicy.SOURCE)
diff --git a/tests/FlickerTests/test-apps/app-helpers/OWNERS b/tests/FlickerTests/test-apps/app-helpers/OWNERS
new file mode 100644
index 0000000..ab62532
--- /dev/null
+++ b/tests/FlickerTests/test-apps/app-helpers/OWNERS
@@ -0,0 +1,2 @@
+uysalorhan@google.com
+pragyabajoria@google.com
\ No newline at end of file
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
new file mode 100644
index 0000000..461dfec
--- /dev/null
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.wm.flicker.helpers
+
+import android.tools.device.apphelpers.IStandardAppHelper
+import android.tools.helpers.SYSTEMUI_PACKAGE
+import android.tools.traces.parsers.WindowManagerStateHelper
+import android.tools.traces.wm.WindowingMode
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+
+/**
+ * Wrapper class around App helper classes. This class adds functionality to the apps that the
+ * desktop apps would have.
+ */
+open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) :
+ IStandardAppHelper by innerHelper {
+ private val TIMEOUT_MS = 3_000L
+ private val CAPTION = "desktop_mode_caption"
+ private val CAPTION_HANDLE = "caption_handle"
+ private val MAXIMIZE_BUTTON = "maximize_window"
+ private val MAXIMIZE_BUTTON_VIEW = "maximize_button_view"
+ private val CLOSE_BUTTON = "close_window"
+
+ private val caption: BySelector
+ get() = By.res(SYSTEMUI_PACKAGE, CAPTION)
+
+ /** Wait for an app moved to desktop to finish its transition. */
+ private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) {
+ wmHelper
+ .StateSyncBuilder()
+ .withWindowSurfaceAppeared(innerHelper)
+ .withFreeformApp(innerHelper)
+ .withAppTransitionIdle()
+ .waitForAndVerify()
+ }
+
+ /** Move an app to Desktop by dragging the app handle at the top. */
+ fun enterDesktopWithDrag(
+ wmHelper: WindowManagerStateHelper,
+ device: UiDevice,
+ ) {
+ innerHelper.launchViaIntent(wmHelper)
+ dragToDesktop(wmHelper, device)
+ waitForAppToMoveToDesktop(wmHelper)
+ }
+
+ private fun dragToDesktop(wmHelper: WindowManagerStateHelper, device: UiDevice) {
+ val windowRect = wmHelper.getWindowRegion(innerHelper).bounds
+ val startX = windowRect.centerX()
+
+ // Start dragging a little under the top to prevent dragging the notification shade.
+ val startY = 10
+
+ val displayRect =
+ wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect
+ ?: throw IllegalStateException("Default display is null")
+
+ // The position we want to drag to
+ val endY = displayRect.centerY() / 2
+
+ // drag the window to move to desktop
+ device.drag(startX, startY, startX, endY, 100)
+ }
+
+ /** Click maximise button on the app header for the given app. */
+ fun maximiseDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) {
+ val caption = getCaptionForTheApp(wmHelper, device)
+ val maximizeButton =
+ caption
+ ?.children
+ ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) }
+ ?.children
+ ?.get(0)
+ maximizeButton?.click()
+ wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify()
+ }
+ /** Click close button on the app header for the given app. */
+ fun closeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) {
+ val caption = getCaptionForTheApp(wmHelper, device)
+ val closeButton = caption?.children?.find { it.resourceName.endsWith(CLOSE_BUTTON) }
+ closeButton?.click()
+ wmHelper
+ .StateSyncBuilder()
+ .withAppTransitionIdle()
+ .withWindowSurfaceDisappeared(innerHelper)
+ .waitForAndVerify()
+ }
+
+ private fun getCaptionForTheApp(
+ wmHelper: WindowManagerStateHelper,
+ device: UiDevice
+ ): UiObject2? {
+ if (
+ wmHelper.getWindow(innerHelper)?.windowingMode !=
+ WindowingMode.WINDOWING_MODE_FREEFORM.value
+ )
+ error("expected a freeform window with caption but window is not in freeform mode")
+ val captions =
+ device.wait(Until.findObjects(caption), TIMEOUT_MS)
+ ?: error("Unable to find view $caption\n")
+
+ return captions.find {
+ wmHelper.getWindowRegion(innerHelper).bounds.contains(it.visibleBounds)
+ }
+ }
+}