Merge "Fix PeopleSpace when showing only priority or recent conversations (1/2)" into tm-qpr-dev
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index 3f6046f..53a84bd 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -583,6 +583,15 @@
mFlingerSurface = s;
mTargetInset = -1;
+ // Rotate the boot animation according to the value specified in the sysprop
+ // ro.bootanim.set_orientation_<display_id>. Four values are supported: ORIENTATION_0,
+ // ORIENTATION_90, ORIENTATION_180 and ORIENTATION_270.
+ // If the value isn't specified or is ORIENTATION_0, nothing will be changed.
+ // This is needed to support having boot animation in orientations different from the natural
+ // device orientation. For example, on tablets that may want to keep natural orientation
+ // portrait for applications compatibility and to have the boot animation in landscape.
+ rotateAwayFromNaturalOrientationIfNeeded();
+
projectSceneToWindow();
// Register a display event receiver
@@ -596,6 +605,50 @@
return NO_ERROR;
}
+void BootAnimation::rotateAwayFromNaturalOrientationIfNeeded() {
+ const auto orientation = parseOrientationProperty();
+
+ if (orientation == ui::ROTATION_0) {
+ // Do nothing if the sysprop isn't set or is set to ROTATION_0.
+ return;
+ }
+
+ if (orientation == ui::ROTATION_90 || orientation == ui::ROTATION_270) {
+ std::swap(mWidth, mHeight);
+ std::swap(mInitWidth, mInitHeight);
+ mFlingerSurfaceControl->updateDefaultBufferSize(mWidth, mHeight);
+ }
+
+ Rect displayRect(0, 0, mWidth, mHeight);
+ Rect layerStackRect(0, 0, mWidth, mHeight);
+
+ SurfaceComposerClient::Transaction t;
+ t.setDisplayProjection(mDisplayToken, orientation, layerStackRect, displayRect);
+ t.apply();
+}
+
+ui::Rotation BootAnimation::parseOrientationProperty() {
+ const auto displayIds = SurfaceComposerClient::getPhysicalDisplayIds();
+ if (displayIds.size() == 0) {
+ return ui::ROTATION_0;
+ }
+ const auto displayId = displayIds[0];
+ const auto syspropName = [displayId] {
+ std::stringstream ss;
+ ss << "ro.bootanim.set_orientation_" << displayId.value;
+ return ss.str();
+ }();
+ const auto syspropValue = android::base::GetProperty(syspropName, "ORIENTATION_0");
+ if (syspropValue == "ORIENTATION_90") {
+ return ui::ROTATION_90;
+ } else if (syspropValue == "ORIENTATION_180") {
+ return ui::ROTATION_180;
+ } else if (syspropValue == "ORIENTATION_270") {
+ return ui::ROTATION_270;
+ }
+ return ui::ROTATION_0;
+}
+
void BootAnimation::projectSceneToWindow() {
glViewport(0, 0, mWidth, mHeight);
glScissor(0, 0, mWidth, mHeight);
diff --git a/cmds/bootanimation/BootAnimation.h b/cmds/bootanimation/BootAnimation.h
index 8658205..8683b71 100644
--- a/cmds/bootanimation/BootAnimation.h
+++ b/cmds/bootanimation/BootAnimation.h
@@ -30,6 +30,8 @@
#include <utils/Thread.h>
#include <binder/IBinder.h>
+#include <ui/Rotation.h>
+
#include <EGL/egl.h>
#include <GLES2/gl2.h>
@@ -200,6 +202,8 @@
ui::Size limitSurfaceSize(int width, int height) const;
void resizeSurface(int newWidth, int newHeight);
void projectSceneToWindow();
+ void rotateAwayFromNaturalOrientationIfNeeded();
+ ui::Rotation parseOrientationProperty();
bool shouldStopPlayingPart(const Animation::Part& part, int fadedFramesCount,
int lastDisplayedProgress);
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 324b8e7..0074a0d 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -58,6 +58,15 @@
}
}
+ /** Reports {@link android.app.servertransaction.RefreshCallbackItem} is executed. */
+ public void activityRefreshed(IBinder token) {
+ try {
+ getActivityClientController().activityRefreshed(token);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Reports after {@link Activity#onTopResumedActivityChanged(boolean)} is called for losing the
* top most position.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index ef6c5a6..2beb64d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -5249,6 +5249,11 @@
}
}
+ @Override
+ public void reportRefresh(ActivityClientRecord r) {
+ ActivityClient.getInstance().activityRefreshed(r.token);
+ }
+
private void handleSetCoreSettings(Bundle coreSettings) {
synchronized (mCoreSettingsLock) {
mCoreSettings = coreSettings;
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index a7566fd..2c70c4e 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -140,6 +140,9 @@
/** Restart the activity after it was stopped. */
public abstract void performRestartActivity(@NonNull ActivityClientRecord r, boolean start);
+ /** Report that activity was refreshed to server. */
+ public abstract void reportRefresh(@NonNull ActivityClientRecord r);
+
/** Set pending activity configuration in case it will be updated by other transaction item. */
public abstract void updatePendingActivityConfiguration(@NonNull IBinder token,
Configuration overrideConfig);
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 8b655b9..969f975 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -38,6 +38,7 @@
interface IActivityClientController {
oneway void activityIdle(in IBinder token, in Configuration config, in boolean stopProfiling);
oneway void activityResumed(in IBinder token, in boolean handleSplashScreenExit);
+ oneway void activityRefreshed(in IBinder token);
/**
* This call is not one-way because {@link #activityPaused()) is not one-way, or
* the top-resumed-lost could be reported after activity paused.
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 162a997..7eacc3c 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -1096,7 +1096,7 @@
}
/**
- * Like {@link #getFastDrawable(int)}, but the returned Drawable has a number
+ * Like {@link #getDrawable(int)}, but the returned Drawable has a number
* of limitations to reduce its overhead as much as possible. It will
* never scale the wallpaper (only centering it if the requested bounds
* do match the bitmap bounds, which should not be typical), doesn't
diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java
index d94f08b..b159f33 100644
--- a/core/java/android/app/servertransaction/ClientTransactionItem.java
+++ b/core/java/android/app/servertransaction/ClientTransactionItem.java
@@ -38,6 +38,9 @@
return UNDEFINED;
}
+ boolean shouldHaveDefinedPreExecutionState() {
+ return true;
+ }
// Parcelable
diff --git a/core/java/android/app/servertransaction/RefreshCallbackItem.java b/core/java/android/app/servertransaction/RefreshCallbackItem.java
new file mode 100644
index 0000000..74abab2
--- /dev/null
+++ b/core/java/android/app/servertransaction/RefreshCallbackItem.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 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.app.servertransaction;
+
+import static android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread.ActivityClientRecord;
+import android.app.ClientTransactionHandler;
+import android.os.IBinder;
+import android.os.Parcel;
+
+/**
+ * Callback that allows to {@link TransactionExecutor#cycleToPath} to {@link ON_PAUSE} or
+ * {@link ON_STOP} in {@link TransactionExecutor#executeCallbacks} for activity "refresh" flow
+ * that goes through "paused -> resumed" or "stopped -> resumed" cycle.
+ *
+ * <p>This is used in combination with {@link com.android.server.wm.DisplayRotationCompatPolicy}
+ * for camera compatibility treatment that handles orientation mismatch between camera buffers and
+ * an app window. This allows to clear cached values in apps (e.g. display or camera rotation) that
+ * influence camera preview and can lead to sideways or stretching issues.
+ *
+ * @hide
+ */
+public class RefreshCallbackItem extends ActivityTransactionItem {
+
+ // Whether refresh should happen using the "stopped -> resumed" cycle or
+ // "paused -> resumed" cycle.
+ @LifecycleState
+ private int mPostExecutionState;
+
+ @Override
+ public void execute(@NonNull ClientTransactionHandler client,
+ @NonNull ActivityClientRecord r, PendingTransactionActions pendingActions) {}
+
+ @Override
+ public void postExecute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ final ActivityClientRecord r = getActivityClientRecord(client, token);
+ client.reportRefresh(r);
+ }
+
+ @Override
+ public int getPostExecutionState() {
+ return mPostExecutionState;
+ }
+
+ @Override
+ boolean shouldHaveDefinedPreExecutionState() {
+ return false;
+ }
+
+ // ObjectPoolItem implementation
+
+ @Override
+ public void recycle() {
+ ObjectPool.recycle(this);
+ }
+
+ /**
+ * Obtain an instance initialized with provided params.
+ * @param postExecutionState indicating whether refresh should happen using the
+ * "stopped -> resumed" cycle or "paused -> resumed" cycle.
+ */
+ public static RefreshCallbackItem obtain(@LifecycleState int postExecutionState) {
+ if (postExecutionState != ON_STOP && postExecutionState != ON_PAUSE) {
+ throw new IllegalArgumentException(
+ "Only ON_STOP or ON_PAUSE are allowed as a post execution state for "
+ + "RefreshCallbackItem but got " + postExecutionState);
+ }
+ RefreshCallbackItem instance =
+ ObjectPool.obtain(RefreshCallbackItem.class);
+ if (instance == null) {
+ instance = new RefreshCallbackItem();
+ }
+ instance.mPostExecutionState = postExecutionState;
+ return instance;
+ }
+
+ private RefreshCallbackItem() {}
+
+ // Parcelable implementation
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mPostExecutionState);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final RefreshCallbackItem other = (RefreshCallbackItem) o;
+ return mPostExecutionState == other.mPostExecutionState;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + mPostExecutionState;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "RefreshCallbackItem{mPostExecutionState=" + mPostExecutionState + "}";
+ }
+
+ private RefreshCallbackItem(Parcel in) {
+ mPostExecutionState = in.readInt();
+ }
+
+ public static final @NonNull Creator<RefreshCallbackItem> CREATOR =
+ new Creator<RefreshCallbackItem>() {
+
+ public RefreshCallbackItem createFromParcel(Parcel in) {
+ return new RefreshCallbackItem(in);
+ }
+
+ public RefreshCallbackItem[] newArray(int size) {
+ return new RefreshCallbackItem[size];
+ }
+ };
+}
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index de1d38a..1ff0b79 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -126,10 +126,13 @@
final ClientTransactionItem item = callbacks.get(i);
if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
final int postExecutionState = item.getPostExecutionState();
- final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
- item.getPostExecutionState());
- if (closestPreExecutionState != UNDEFINED) {
- cycleToPath(r, closestPreExecutionState, transaction);
+
+ if (item.shouldHaveDefinedPreExecutionState()) {
+ final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
+ item.getPostExecutionState());
+ if (closestPreExecutionState != UNDEFINED) {
+ cycleToPath(r, closestPreExecutionState, transaction);
+ }
}
item.execute(mTransactionHandler, token, mPendingActions);
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 30b7d25..be99f0f 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -114,7 +114,7 @@
*/
@ChangeId
@Overridable
- @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU)
+ @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE)
@TestApi
public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L;
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 68e1acb..132bd66 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -608,7 +608,10 @@
WAKE_REASON_DISPLAY_GROUP_ADDED,
WAKE_REASON_DISPLAY_GROUP_TURNED_ON,
WAKE_REASON_UNFOLD_DEVICE,
- WAKE_REASON_DREAM_FINISHED
+ WAKE_REASON_DREAM_FINISHED,
+ WAKE_REASON_TAP,
+ WAKE_REASON_LIFT,
+ WAKE_REASON_BIOMETRIC,
})
@Retention(RetentionPolicy.SOURCE)
public @interface WakeReason{}
@@ -660,8 +663,9 @@
public static final int WAKE_REASON_PLUGGED_IN = 3;
/**
- * Wake up reason code: Waking up due to a user performed gesture (e.g. double tapping on the
- * screen).
+ * Wake up reason code: Waking up due to a user performed gesture. This includes user
+ * interactions with UI on the screen such as the notification shade. This does not include
+ * {@link WAKE_REASON_TAP} or {@link WAKE_REASON_LIFT}.
* @hide
*/
public static final int WAKE_REASON_GESTURE = 4;
@@ -723,6 +727,26 @@
public static final int WAKE_REASON_DREAM_FINISHED = 13;
/**
+ * Wake up reason code: Waking up due to the user single or double tapping on the screen. This
+ * wake reason is used when the user is not tapping on a specific UI element; rather, the device
+ * wakes up due to a generic tap on the screen.
+ * @hide
+ */
+ public static final int WAKE_REASON_TAP = 15;
+
+ /**
+ * Wake up reason code: Waking up due to a user performed lift gesture.
+ * @hide
+ */
+ public static final int WAKE_REASON_LIFT = 16;
+
+ /**
+ * Wake up reason code: Waking up due to a user interacting with a biometric.
+ * @hide
+ */
+ public static final int WAKE_REASON_BIOMETRIC = 17;
+
+ /**
* Convert the wake reason to a string for debugging purposes.
* @hide
*/
@@ -742,6 +766,9 @@
case WAKE_REASON_DISPLAY_GROUP_TURNED_ON: return "WAKE_REASON_DISPLAY_GROUP_TURNED_ON";
case WAKE_REASON_UNFOLD_DEVICE: return "WAKE_REASON_UNFOLD_DEVICE";
case WAKE_REASON_DREAM_FINISHED: return "WAKE_REASON_DREAM_FINISHED";
+ case WAKE_REASON_TAP: return "WAKE_REASON_TAP";
+ case WAKE_REASON_LIFT: return "WAKE_REASON_LIFT";
+ case WAKE_REASON_BIOMETRIC: return "WAKE_REASON_BIOMETRIC";
default: return Integer.toString(wakeReason);
}
}
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index 950c8ac..ed24740 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -58,19 +58,24 @@
* Manifest metadata to show a custom embedded activity as part of device controls.
*
* The value of this metadata must be the {@link ComponentName} as a string of an activity in
- * the same package that will be launched as part of a TaskView.
+ * the same package that will be launched embedded in the device controls space.
*
* The activity must be exported, enabled and protected by
* {@link Manifest.permission.BIND_CONTROLS}.
*
+ * It is recommended that the activity is declared {@code android:resizeableActivity="true"}.
+ *
* @hide
*/
public static final String META_DATA_PANEL_ACTIVITY =
"android.service.controls.META_DATA_PANEL_ACTIVITY";
/**
- * Boolean extra containing the value of
- * {@link android.provider.Settings.Secure#LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS}.
+ * Boolean extra containing the value of the setting allowing actions on a locked device.
+ *
+ * This corresponds to the setting that indicates whether the user has
+ * consented to allow actions on devices that declare {@link Control#isAuthRequired()} as
+ * {@code false} when the device is locked.
*
* This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY}
* is launched.
@@ -78,7 +83,7 @@
* @hide
*/
public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS =
- "android.service.controls.extra.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
+ "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
/**
* @hide
diff --git a/core/java/android/service/controls/OWNERS b/core/java/android/service/controls/OWNERS
new file mode 100644
index 0000000..4bb78c7
--- /dev/null
+++ b/core/java/android/service/controls/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 802726
+asc@google.com
+kozynski@google.com
+juliacr@google.com
\ No newline at end of file
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index a892570..bffa660 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -79,6 +79,12 @@
/** @hide */
public static final int PRIORITY_CATEGORY_CONVERSATIONS = 8;
+ /**
+ * Total number of priority categories. Keep updated with any updates to PriorityCategory enum.
+ * @hide
+ */
+ public static final int NUM_PRIORITY_CATEGORIES = 9;
+
/** @hide */
@IntDef(prefix = { "VISUAL_EFFECT_" }, value = {
VISUAL_EFFECT_FULL_SCREEN_INTENT,
@@ -107,6 +113,12 @@
/** @hide */
public static final int VISUAL_EFFECT_NOTIFICATION_LIST = 6;
+ /**
+ * Total number of visual effects. Keep updated with any updates to VisualEffect enum.
+ * @hide
+ */
+ public static final int NUM_VISUAL_EFFECTS = 7;
+
/** @hide */
@IntDef(prefix = { "PEOPLE_TYPE_" }, value = {
PEOPLE_TYPE_UNSET,
@@ -202,8 +214,8 @@
/** @hide */
public ZenPolicy() {
- mPriorityCategories = new ArrayList<>(Collections.nCopies(9, 0));
- mVisualEffects = new ArrayList<>(Collections.nCopies(7, 0));
+ mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0));
+ mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0));
}
/**
@@ -804,8 +816,12 @@
@Override
public ZenPolicy createFromParcel(Parcel source) {
ZenPolicy policy = new ZenPolicy();
- policy.mPriorityCategories = source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class);
- policy.mVisualEffects = source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class);
+ policy.mPriorityCategories = trimList(
+ source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class),
+ NUM_PRIORITY_CATEGORIES);
+ policy.mVisualEffects = trimList(
+ source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class),
+ NUM_VISUAL_EFFECTS);
policy.mPriorityCalls = source.readInt();
policy.mPriorityMessages = source.readInt();
policy.mConversationSenders = source.readInt();
@@ -832,6 +848,15 @@
.toString();
}
+ // Returns a list containing the first maxLength elements of the input list if the list is
+ // longer than that size. For the lists in ZenPolicy, this should not happen unless the input
+ // is corrupt.
+ private static ArrayList<Integer> trimList(ArrayList<Integer> list, int maxLength) {
+ if (list == null || list.size() <= maxLength) {
+ return list;
+ }
+ return new ArrayList<>(list.subList(0, maxLength));
+ }
private String priorityCategoriesToString() {
StringBuilder builder = new StringBuilder();
diff --git a/core/java/android/service/wallpaper/OWNERS b/core/java/android/service/wallpaper/OWNERS
index 756eef8..71bd190 100644
--- a/core/java/android/service/wallpaper/OWNERS
+++ b/core/java/android/service/wallpaper/OWNERS
@@ -3,3 +3,6 @@
dupin@google.com
dsandler@android.com
dsandler@google.com
+pomini@google.com
+poultney@google.com
+santie@google.com
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index 14f5228..9152e78 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -123,7 +123,7 @@
private void receive(
int resultCode, Bundle resultData,
- @NonNull OnBackInvokedDispatcher receivingDispatcher) {
+ @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) {
final int callbackId = resultData.getInt(RESULT_KEY_ID);
if (resultCode == RESULT_CODE_REGISTER) {
int priority = resultData.getInt(RESULT_KEY_PRIORITY);
@@ -140,11 +140,11 @@
@NonNull IOnBackInvokedCallback iCallback,
@OnBackInvokedDispatcher.Priority int priority,
int callbackId,
- @NonNull OnBackInvokedDispatcher receivingDispatcher) {
+ @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) {
final ImeOnBackInvokedCallback imeCallback =
new ImeOnBackInvokedCallback(iCallback, callbackId, priority);
mImeCallbacks.add(imeCallback);
- receivingDispatcher.registerOnBackInvokedCallback(priority, imeCallback);
+ receivingDispatcher.registerOnBackInvokedCallbackUnchecked(imeCallback, priority);
}
private void unregisterReceivedCallback(
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 22f4298..a0d6e47 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5204,6 +5204,15 @@
the format [System DeviceState]:[WM Jetpack Posture], for example: "0:1". -->
<string-array name="config_device_state_postures" translatable="false" />
+ <!-- Which Surface rotations are considered as tabletop posture (horizontal hinge) when the
+ device is half-folded. Other half-folded postures will be assumed to be book (vertical
+ hinge) mode. Units: degrees; valid values: 0, 90, 180, 270. -->
+ <integer-array name="config_deviceTabletopRotations" />
+
+ <!-- This flag indicates that a display with fold-state FLAT should always be considered as
+ having a separating hinge. -->
+ <bool name="config_isDisplayHingeAlwaysSeparating">false</bool>
+
<!-- Aspect ratio of letterboxing for fixed orientation. Values <= 1.0 will be ignored.
Note: Activity min/max aspect ratio restrictions will still be respected.
Therefore this override can control the maximum screen area that can be occupied by
@@ -5252,14 +5261,26 @@
<!-- Horizontal position of a center of the letterboxed app window.
0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
- or > 1, it is ignored and central position is used (0.5). -->
+ or > 1 it is ignored and for non-book mode central position is used (0.5); for book mode
+ left is used (0.0). -->
<item name="config_letterboxHorizontalPositionMultiplier" format="float" type="dimen">0.5</item>
<!-- Vertical position of a center of the letterboxed app window.
0 corresponds to the upper side of the screen and 1 to the lower side. If given value < 0
- or > 1, it is ignored and central position is used (0.5). -->
+ or > 1 it is ignored and for non-tabletop mode central position is used (0.5); for
+ tabletop mode top (0.0) is used. -->
<item name="config_letterboxVerticalPositionMultiplier" format="float" type="dimen">0.0</item>
+ <!-- Horizontal position of a center of the letterboxed app window when in book mode.
+ 0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
+ or > 1, it is ignored and left position is used (0.0). -->
+ <item name="config_letterboxBookModePositionMultiplier" format="float" type="dimen">0.0</item>
+
+ <!-- Vertical position of a center of the letterboxed app window when in tabletop mode.
+ 0 corresponds to the upper side of the screen and 1 to the lower side. If given value < 0
+ or > 1, it is ignored and top position is used (0.0). -->
+ <item name="config_letterboxTabletopModePositionMultiplier" format="float" type="dimen">0.0</item>
+
<!-- Whether horizontal reachability repositioning is allowed for letterboxed fullscreen apps.
-->
<bool name="config_letterboxIsHorizontalReachabilityEnabled">false</bool>
@@ -5287,6 +5308,26 @@
If given value is outside of this range, the option 1 (center) is assummed. -->
<integer name="config_letterboxDefaultPositionForVerticalReachability">1</integer>
+ <!-- Default horizontal position of the letterboxed app window when reachability is
+ enabled and an app is fullscreen in landscape device orientation and in book mode. When
+ reachability is enabled, the position can change between left, center and right. This config
+ defines the default one:
+ - Option 0 - Left.
+ - Option 1 - Center.
+ - Option 2 - Right.
+ If given value is outside of this range, the option 0 (left) is assummed. -->
+ <integer name="config_letterboxDefaultPositionForBookModeReachability">0</integer>
+
+ <!-- Default vertical position of the letterboxed app window when reachability is
+ enabled and an app is fullscreen in portrait device orientation and in tabletop mode. When
+ reachability is enabled, the position can change between top, center and bottom. This config
+ defines the default one:
+ - Option 0 - Top.
+ - Option 1 - Center.
+ - Option 2 - Bottom.
+ If given value is outside of this range, the option 0 (top) is assummed. -->
+ <integer name="config_letterboxDefaultPositionForTabletopModeReachability">0</integer>
+
<!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. -->
<bool name="config_letterboxIsEducationEnabled">false</bool>
@@ -5303,6 +5344,12 @@
TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
<bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
+ <!-- Whether camera compat treatment is enabled for issues caused by orientation mismatch
+ between camera buffers and an app window. This includes force rotation of fixed
+ orientation activities connected to the camera in fullscreen and showing a tooltip in
+ split screen. -->
+ <bool name="config_isWindowManagerCameraCompatTreatmentEnabled">false</bool>
+
<!-- Whether a camera compat controller is enabled to allow the user to apply or revert
treatment for stretched issues in camera viewfinder. -->
<bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ba274a7..371dbfb 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4435,6 +4435,8 @@
<java-symbol type="array" name="config_keep_warming_services" />
<java-symbol type="string" name="config_display_features" />
<java-symbol type="array" name="config_device_state_postures" />
+ <java-symbol type="array" name="config_deviceTabletopRotations" />
+ <java-symbol type="bool" name="config_isDisplayHingeAlwaysSeparating" />
<java-symbol type="dimen" name="controls_thumbnail_image_max_height" />
<java-symbol type="dimen" name="controls_thumbnail_image_max_width" />
@@ -4447,13 +4449,18 @@
<java-symbol type="color" name="config_letterboxBackgroundColor" />
<java-symbol type="dimen" name="config_letterboxHorizontalPositionMultiplier" />
<java-symbol type="dimen" name="config_letterboxVerticalPositionMultiplier" />
+ <java-symbol type="dimen" name="config_letterboxBookModePositionMultiplier" />
+ <java-symbol type="dimen" name="config_letterboxTabletopModePositionMultiplier" />
<java-symbol type="bool" name="config_letterboxIsHorizontalReachabilityEnabled" />
<java-symbol type="bool" name="config_letterboxIsVerticalReachabilityEnabled" />
<java-symbol type="integer" name="config_letterboxDefaultPositionForHorizontalReachability" />
<java-symbol type="integer" name="config_letterboxDefaultPositionForVerticalReachability" />
+ <java-symbol type="integer" name="config_letterboxDefaultPositionForBookModeReachability" />
+ <java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" />
<java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
<java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
<java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
+ <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
<java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
<java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
diff --git a/core/tests/coretests/src/android/service/controls/OWNERS b/core/tests/coretests/src/android/service/controls/OWNERS
new file mode 100644
index 0000000..bf67034
--- /dev/null
+++ b/core/tests/coretests/src/android/service/controls/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/core/java/android/service/controls/OWNERS
\ No newline at end of file
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 86c8097..f47d9c6 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -295,6 +295,12 @@
"group": "WM_DEBUG_IME",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
+ "-1812743677": {
+ "message": "Display id=%d is ignoring all orientation requests, camera is active and the top activity is eligible for force rotation, return %s,portrait activity: %b, is natural orientation portrait: %b.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-1810446914": {
"message": "Trying to update display configuration for system\/invalid process.",
"level": "WARN",
@@ -481,6 +487,12 @@
"group": "WM_DEBUG_ORIENTATION",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
+ "-1631991057": {
+ "message": "Display id=%d is notified that Camera %s is closed but activity is still refreshing. Rescheduling an update.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-1630752478": {
"message": "removeLockedTask: removed %s",
"level": "DEBUG",
@@ -619,6 +631,12 @@
"group": "WM_DEBUG_WINDOW_INSETS",
"at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
},
+ "-1480918485": {
+ "message": "Refreshed activity: %s",
+ "level": "INFO",
+ "group": "WM_DEBUG_STATES",
+ "at": "com\/android\/server\/wm\/ActivityRecord.java"
+ },
"-1480772131": {
"message": "No app or window is requesting an orientation, return %d for display id=%d",
"level": "VERBOSE",
@@ -1321,6 +1339,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "-799396645": {
+ "message": "Display id=%d is notified that Camera %s is closed, updating rotation.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-799003045": {
"message": "Set animatingExit: reason=remove\/replaceWindow win=%s",
"level": "VERBOSE",
@@ -1525,6 +1549,12 @@
"group": "WM_DEBUG_FOCUS_LIGHT",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
+ "-637815408": {
+ "message": "Invalid surface rotation angle in config_deviceTabletopRotations: %d",
+ "level": "ERROR",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotation.java"
+ },
"-636553602": {
"message": "commitVisibility: %s: visible=%b visibleRequested=%b, isInTransition=%b, runningAnimation=%b, caller=%s",
"level": "VERBOSE",
@@ -1537,6 +1567,12 @@
"group": "WM_DEBUG_SCREEN_ON",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
+ "-627759820": {
+ "message": "Display id=%d is notified that Camera %s is open for package %s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-622997754": {
"message": "postWindowRemoveCleanupLocked: %s",
"level": "VERBOSE",
@@ -2095,6 +2131,12 @@
"group": "WM_SHOW_TRANSACTIONS",
"at": "com\/android\/server\/wm\/Session.java"
},
+ "-81260230": {
+ "message": "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-81121442": {
"message": "ImeContainer just became organized but it doesn't have a parent or the parent doesn't have a surface control. mSurfaceControl=%s imeParentSurfaceControl=%s",
"level": "ERROR",
@@ -3193,6 +3235,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/TaskFragment.java"
},
+ "939638078": {
+ "message": "config_deviceTabletopRotations is not defined. Half-fold letterboxing will work inconsistently.",
+ "level": "WARN",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotation.java"
+ },
"948208142": {
"message": "Setting Activity.mLauncherTaskBehind to true. Activity=%s",
"level": "DEBUG",
@@ -4189,6 +4237,12 @@
"group": "WM_DEBUG_REMOTE_ANIMATIONS",
"at": "com\/android\/server\/wm\/RemoteAnimationController.java"
},
+ "1967643923": {
+ "message": "Refershing activity for camera compatibility treatment, activityRecord=%s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_STATES",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"1967975839": {
"message": "Changing app %s visible=%b performLayout=%b",
"level": "VERBOSE",
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 23db233..774f6c6 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -115,4 +115,10 @@
<!-- Components support to launch multiple instances into split-screen -->
<string-array name="config_appsSupportMultiInstancesSplit">
</string-array>
+
+ <!-- Whether the extended restart dialog is enabled -->
+ <bool name="config_letterboxIsRestartDialogEnabled">false</bool>
+
+ <!-- Whether the additional education about reachability is enabled -->
+ <bool name="config_letterboxIsReachabilityEducationEnabled">false</bool>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
new file mode 100644
index 0000000..4f33a71
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 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.compatui;
+
+import android.content.Context;
+import android.provider.DeviceConfig;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.dagger.WMSingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Configuration flags for the CompatUX implementation
+ */
+@WMSingleton
+public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedListener {
+
+ static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG = "enable_letterbox_restart_dialog";
+
+ static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION =
+ "enable_letterbox_reachability_education";
+
+ // Whether the extended restart dialog is enabled
+ private boolean mIsRestartDialogEnabled;
+
+ // Whether the additional education about reachability is enabled
+ private boolean mIsReachabilityEducationEnabled;
+
+ // Whether the extended restart dialog is enabled
+ private boolean mIsRestartDialogOverrideEnabled;
+
+ // Whether the additional education about reachability is enabled
+ private boolean mIsReachabilityEducationOverrideEnabled;
+
+ // Whether the extended restart dialog is allowed from backend
+ private boolean mIsLetterboxRestartDialogAllowed;
+
+ // Whether the additional education about reachability is allowed from backend
+ private boolean mIsLetterboxReachabilityEducationAllowed;
+
+ @Inject
+ public CompatUIConfiguration(Context context, @ShellMainThread ShellExecutor mainExecutor) {
+ mIsRestartDialogEnabled = context.getResources().getBoolean(
+ R.bool.config_letterboxIsRestartDialogEnabled);
+ mIsReachabilityEducationEnabled = context.getResources().getBoolean(
+ R.bool.config_letterboxIsReachabilityEducationEnabled);
+ mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, false);
+ mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION,
+ false);
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor,
+ this);
+ }
+
+ /**
+ * @return {@value true} if the restart dialog is enabled.
+ */
+ boolean isRestartDialogEnabled() {
+ return mIsRestartDialogOverrideEnabled || (mIsRestartDialogEnabled
+ && mIsLetterboxRestartDialogAllowed);
+ }
+
+ /**
+ * Enables/Disables the restart education dialog
+ */
+ void setIsRestartDialogOverrideEnabled(boolean enabled) {
+ mIsRestartDialogOverrideEnabled = enabled;
+ }
+
+ /**
+ * @return {@value true} if the reachability education is enabled.
+ */
+ boolean isReachabilityEducationEnabled() {
+ return mIsReachabilityEducationOverrideEnabled || (mIsReachabilityEducationEnabled
+ && mIsLetterboxReachabilityEducationAllowed);
+ }
+
+ /**
+ * Enables/Disables the reachability education
+ */
+ void setIsReachabilityEducationOverrideEnabled(boolean enabled) {
+ mIsReachabilityEducationOverrideEnabled = enabled;
+ }
+
+ @Override
+ public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+ // TODO(b/263349751): Update flag and default value to true
+ if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) {
+ mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG,
+ false);
+ }
+ if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION)) {
+ mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, false);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java
new file mode 100644
index 0000000..4fb18e2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 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.compatui;
+
+import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * Handles the shell commands for the CompatUX.
+ *
+ * <p> Use with {@code adb shell dumpsys activity service SystemUIService WMShell compatui
+ * <command>}.
+ */
+@WMSingleton
+public final class CompatUIShellCommandHandler implements
+ ShellCommandHandler.ShellCommandActionHandler {
+
+ private final CompatUIConfiguration mCompatUIConfiguration;
+ private final ShellCommandHandler mShellCommandHandler;
+
+ @Inject
+ public CompatUIShellCommandHandler(ShellCommandHandler shellCommandHandler,
+ CompatUIConfiguration compatUIConfiguration) {
+ mShellCommandHandler = shellCommandHandler;
+ mCompatUIConfiguration = compatUIConfiguration;
+ }
+
+ void onInit() {
+ mShellCommandHandler.addCommandCallback("compatui", this, this);
+ }
+
+ @Override
+ public boolean onShellCommand(String[] args, PrintWriter pw) {
+ if (args.length != 2) {
+ pw.println("Invalid command: " + args[0]);
+ return false;
+ }
+ switch (args[0]) {
+ case "restartDialogEnabled":
+ return invokeOrError(args[1], pw,
+ mCompatUIConfiguration::setIsRestartDialogOverrideEnabled);
+ case "reachabilityEducationEnabled":
+ return invokeOrError(args[1], pw,
+ mCompatUIConfiguration::setIsReachabilityEducationOverrideEnabled);
+ default:
+ pw.println("Invalid command: " + args[0]);
+ return false;
+ }
+ }
+
+ @Override
+ public void printShellCommandHelp(PrintWriter pw, String prefix) {
+ pw.println(prefix + "restartDialogEnabled [0|false|1|true]");
+ pw.println(prefix + " Enable/Disable the restart education dialog for Size Compat Mode");
+ pw.println(prefix + "reachabilityEducationEnabled [0|false|1|true]");
+ pw.println(prefix
+ + " Enable/Disable the restart education dialog for letterbox reachability");
+ pw.println(prefix + " Disable the restart education dialog for letterbox reachability");
+ }
+
+ private static boolean invokeOrError(String input, PrintWriter pw,
+ Consumer<Boolean> setter) {
+ Boolean asBoolean = strToBoolean(input);
+ if (asBoolean == null) {
+ pw.println("Error: expected true, 1, false, 0.");
+ return false;
+ }
+ setter.accept(asBoolean);
+ return true;
+ }
+
+ // Converts a String to boolean if possible or it returns null otherwise
+ private static Boolean strToBoolean(String str) {
+ switch(str) {
+ case "1":
+ case "true":
+ return true;
+ case "0":
+ case "false":
+ return false;
+ }
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
similarity index 85%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
index 3061eab..7475fea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.compatui.letterboxedu;
+package com.android.wm.shell.compatui;
import static com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation;
import static com.android.internal.R.styleable.WindowAnimation_windowExitAnimation;
@@ -38,10 +38,15 @@
import com.android.internal.policy.TransitionAnimation;
/**
- * Controls the enter/exit animations of the letterbox education.
+ * Controls the enter/exit a dialog.
+ *
+ * @param <T> The {@link DialogContainerSupplier} to use
*/
-class LetterboxEduAnimationController {
- private static final String TAG = "LetterboxEduAnimation";
+public class DialogAnimationController<T extends DialogContainerSupplier> {
+
+ // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque).
+ // 204 is simply 255 * 0.8.
+ static final int BACKGROUND_DIM_ALPHA = 204;
// If shell transitions are enabled, startEnterAnimation will be called after all transitions
// have finished, and therefore the start delay should be shorter.
@@ -49,6 +54,7 @@
private final TransitionAnimation mTransitionAnimation;
private final String mPackageName;
+ private final String mTag;
@AnyRes
private final int mAnimStyleResId;
@@ -57,23 +63,24 @@
@Nullable
private Animator mBackgroundDimAnimator;
- LetterboxEduAnimationController(Context context) {
- mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, TAG);
+ public DialogAnimationController(Context context, String tag) {
+ mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, tag);
mAnimStyleResId = (new ContextThemeWrapper(context,
android.R.style.ThemeOverlay_Material_Dialog).getTheme()).obtainStyledAttributes(
com.android.internal.R.styleable.Window).getResourceId(
com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
mPackageName = context.getPackageName();
+ mTag = tag;
}
/**
* Starts both background dim fade-in animation and the dialog enter animation.
*/
- void startEnterAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) {
+ public void startEnterAnimation(@NonNull T layout, Runnable endCallback) {
// Cancel any previous animation if it's still running.
cancelAnimation();
- final View dialogContainer = layout.getDialogContainer();
+ final View dialogContainer = layout.getDialogContainerView();
mDialogAnimation = loadAnimation(WindowAnimation_windowEnterAnimation);
if (mDialogAnimation == null) {
endCallback.run();
@@ -86,8 +93,8 @@
endCallback.run();
}));
- mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(),
- /* endAlpha= */ LetterboxEduDialogLayout.BACKGROUND_DIM_ALPHA,
+ mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(),
+ /* endAlpha= */ BACKGROUND_DIM_ALPHA,
mDialogAnimation.getDuration());
mBackgroundDimAnimator.addListener(getDimAnimatorListener());
@@ -101,11 +108,11 @@
/**
* Starts both the background dim fade-out animation and the dialog exit animation.
*/
- void startExitAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) {
+ public void startExitAnimation(@NonNull T layout, Runnable endCallback) {
// Cancel any previous animation if it's still running.
cancelAnimation();
- final View dialogContainer = layout.getDialogContainer();
+ final View dialogContainer = layout.getDialogContainerView();
mDialogAnimation = loadAnimation(WindowAnimation_windowExitAnimation);
if (mDialogAnimation == null) {
endCallback.run();
@@ -119,8 +126,8 @@
endCallback.run();
}));
- mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), /* endAlpha= */ 0,
- mDialogAnimation.getDuration());
+ mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(),
+ /* endAlpha= */ 0, mDialogAnimation.getDuration());
mBackgroundDimAnimator.addListener(getDimAnimatorListener());
dialogContainer.startAnimation(mDialogAnimation);
@@ -130,7 +137,7 @@
/**
* Cancels all animations and resets the state of the controller.
*/
- void cancelAnimation() {
+ public void cancelAnimation() {
if (mDialogAnimation != null) {
mDialogAnimation.cancel();
mDialogAnimation = null;
@@ -145,7 +152,7 @@
Animation animation = mTransitionAnimation.loadAnimationAttr(mPackageName, mAnimStyleResId,
animAttr, /* translucent= */ false);
if (animation == null) {
- Log.e(TAG, "Failed to load animation " + animAttr);
+ Log.e(mTag, "Failed to load animation " + animAttr);
}
return animation;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java
new file mode 100644
index 0000000..7eea446
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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.compatui;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+/**
+ * A component which can provide a {@link View} to use as a container for a Dialog
+ */
+public interface DialogContainerSupplier {
+
+ /**
+ * @return The {@link View} to use as a container for a Dialog
+ */
+ View getDialogContainerView();
+
+ /**
+ * @return The {@link Drawable} to use as background of the dialog.
+ */
+ Drawable getBackgroundDimDrawable();
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
index 2e0b09e..9232f36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
@@ -26,6 +26,7 @@
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.wm.shell.R;
+import com.android.wm.shell.compatui.DialogContainerSupplier;
/**
* Container for Letterbox Education Dialog and background dim.
@@ -33,11 +34,7 @@
* <p>This layout should fill the entire task and the background around the dialog acts as the
* background dim which dismisses the dialog when clicked.
*/
-class LetterboxEduDialogLayout extends ConstraintLayout {
-
- // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque).
- // 204 is simply 255 * 0.8.
- static final int BACKGROUND_DIM_ALPHA = 204;
+class LetterboxEduDialogLayout extends ConstraintLayout implements DialogContainerSupplier {
private View mDialogContainer;
private TextView mDialogTitle;
@@ -60,18 +57,20 @@
super(context, attrs, defStyleAttr, defStyleRes);
}
- View getDialogContainer() {
+ @Override
+ public View getDialogContainerView() {
return mDialogContainer;
}
+ @Override
+ public Drawable getBackgroundDimDrawable() {
+ return mBackgroundDim;
+ }
+
TextView getDialogTitle() {
return mDialogTitle;
}
- Drawable getBackgroundDim() {
- return mBackgroundDim;
- }
-
/**
* Register a callback for the dismiss button and background dim.
*
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
index 867d0ef..c14c009 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
@@ -37,6 +37,7 @@
import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract;
+import com.android.wm.shell.compatui.DialogAnimationController;
import com.android.wm.shell.transition.Transitions;
/**
@@ -63,7 +64,7 @@
*/
private final SharedPreferences mSharedPreferences;
- private final LetterboxEduAnimationController mAnimationController;
+ private final DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
private final Transitions mTransitions;
@@ -96,14 +97,17 @@
DisplayLayout displayLayout, Transitions transitions,
Runnable onDismissCallback, DockStateReader dockStateReader) {
this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
- onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader);
+ onDismissCallback,
+ new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"),
+ dockStateReader);
}
@VisibleForTesting
LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback,
- LetterboxEduAnimationController animationController, DockStateReader dockStateReader) {
+ DialogAnimationController<LetterboxEduDialogLayout> animationController,
+ DockStateReader dockStateReader) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mTransitions = transitions;
mOnDismissCallback = onDismissCallback;
@@ -160,7 +164,7 @@
if (mLayout == null) {
return;
}
- final View dialogContainer = mLayout.getDialogContainer();
+ final View dialogContainer = mLayout.getDialogContainerView();
MarginLayoutParams marginParams = (MarginLayoutParams) dialogContainer.getLayoutParams();
final Rect taskBounds = getTaskBounds();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index b075b14..3341470 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -17,12 +17,20 @@
package com.android.wm.shell.desktopmode
import android.app.ActivityManager
-import android.app.WindowConfiguration
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
-import android.view.WindowManager
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.annotation.BinderThread
import com.android.internal.protolog.common.ProtoLog
@@ -51,7 +59,7 @@
private val transitions: Transitions,
private val desktopModeTaskRepository: DesktopModeTaskRepository,
@ShellMainThread private val mainExecutor: ShellExecutor
-) : RemoteCallable<DesktopTasksController> {
+) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler {
private val desktopMode: DesktopModeImpl
@@ -69,6 +77,7 @@
{ createExternalInterface() },
this
)
+ transitions.addHandler(this)
}
/** Show all tasks, that are part of the desktop, on top of launcher */
@@ -81,7 +90,7 @@
// Execute transaction if there are pending operations
if (!wct.isEmpty) {
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- transitions.startTransition(WindowManager.TRANSIT_TO_FRONT, wct, null /* handler */)
+ transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
} else {
shellTaskOrganizer.applyTransaction(wct)
}
@@ -101,11 +110,11 @@
// Bring other apps to front first
bringDesktopAppsToFront(wct)
- wct.setWindowingMode(task.getToken(), WindowConfiguration.WINDOWING_MODE_FREEFORM)
+ wct.setWindowingMode(task.getToken(), WINDOWING_MODE_FREEFORM)
wct.reorder(task.getToken(), true /* onTop */)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, null /* handler */)
+ transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
} else {
shellTaskOrganizer.applyTransaction(wct)
}
@@ -121,10 +130,10 @@
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId)
val wct = WindowContainerTransaction()
- wct.setWindowingMode(task.getToken(), WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+ wct.setWindowingMode(task.getToken(), WINDOWING_MODE_FULLSCREEN)
wct.setBounds(task.getToken(), null)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, null /* handler */)
+ transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
} else {
shellTaskOrganizer.applyTransaction(wct)
}
@@ -181,6 +190,80 @@
return mainExecutor
}
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback
+ ): Boolean {
+ // This handler should never be the sole handler, so should not animate anything.
+ return false
+ }
+
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo
+ ): WindowContainerTransaction? {
+ // Check if we should skip handling this transition
+ val task: ActivityManager.RunningTaskInfo? = request.triggerTask
+ val shouldHandleRequest =
+ when {
+ // Only handle open or to front transitions
+ request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> false
+ // Only handle when it is a task transition
+ task == null -> false
+ // Only handle standard type tasks
+ task.activityType != ACTIVITY_TYPE_STANDARD -> false
+ // Only handle fullscreen or freeform tasks
+ task.windowingMode != WINDOWING_MODE_FULLSCREEN &&
+ task.windowingMode != WINDOWING_MODE_FREEFORM -> false
+ // Otherwise process it
+ else -> true
+ }
+
+ if (!shouldHandleRequest) {
+ return null
+ }
+
+ val activeTasks = desktopModeTaskRepository.getActiveTasks()
+
+ // Check if we should switch a fullscreen task to freeform
+ if (task?.windowingMode == WINDOWING_MODE_FULLSCREEN) {
+ // If there are any visible desktop tasks, switch the task to freeform
+ if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
+ ProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController#handleRequest: switch fullscreen task to freeform," +
+ " taskId=%d",
+ task.taskId
+ )
+ return WindowContainerTransaction().apply {
+ setWindowingMode(task.token, WINDOWING_MODE_FREEFORM)
+ }
+ }
+ }
+
+ // CHeck if we should switch a freeform task to fullscreen
+ if (task?.windowingMode == WINDOWING_MODE_FREEFORM) {
+ // If no visible desktop tasks, switch this task to freeform as the transition came
+ // outside of this controller
+ if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
+ ProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController#handleRequest: switch freeform task to fullscreen," +
+ " taskId=%d",
+ task.taskId
+ )
+ return WindowContainerTransaction().apply {
+ setWindowingMode(task.token, WINDOWING_MODE_FULLSCREEN)
+ setBounds(task.token, null)
+ }
+ }
+ }
+ return null
+ }
+
/** Creates a new instance of the external interface to pass to another process. */
private fun createExternalInterface(): ExternalInterfaceBinder {
return IDesktopModeImpl(this)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
index 6b59e31..d7cb490 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -16,8 +16,6 @@
package com.android.wm.shell.unfold;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-
import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.TaskInfo;
@@ -56,6 +54,12 @@
private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>();
private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>();
+ /**
+ * Indicates whether we're in stage change process. This should be set to {@code true} in
+ * {@link #onStateChangeStarted()} and {@code false} in {@link #onStateChangeFinished()}.
+ */
+ private boolean mIsInStageChange;
+
public UnfoldAnimationController(
@NonNull ShellInit shellInit,
@NonNull TransactionPool transactionPool,
@@ -123,7 +127,7 @@
animator.onTaskChanged(taskInfo);
} else {
// Became inapplicable
- resetTask(animator, taskInfo);
+ maybeResetTask(animator, taskInfo);
animator.onTaskVanished(taskInfo);
mAnimatorsByTaskId.remove(taskInfo.taskId);
}
@@ -154,7 +158,7 @@
final boolean isCurrentlyApplicable = animator != null;
if (isCurrentlyApplicable) {
- resetTask(animator, taskInfo);
+ maybeResetTask(animator, taskInfo);
animator.onTaskVanished(taskInfo);
mAnimatorsByTaskId.remove(taskInfo.taskId);
}
@@ -166,6 +170,7 @@
return;
}
+ mIsInStageChange = true;
SurfaceControl.Transaction transaction = null;
for (int i = 0; i < mAnimators.size(); i++) {
final UnfoldTaskAnimator animator = mAnimators.get(i);
@@ -219,11 +224,12 @@
transaction.apply();
mTransactionPool.release(transaction);
+ mIsInStageChange = false;
}
- private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
- if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
- // PiP task has its own cleanup path, ignore surface reset to avoid conflict.
+ private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
+ if (!mIsInStageChange) {
+ // No need to resetTask if there is no ongoing state change.
return;
}
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
index 1dee88c..a58620d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
@@ -68,11 +68,11 @@
@Test
public void testOnFinishInflate() {
- assertEquals(mLayout.getDialogContainer(),
+ assertEquals(mLayout.getDialogContainerView(),
mLayout.findViewById(R.id.letterbox_education_dialog_container));
assertEquals(mLayout.getDialogTitle(),
mLayout.findViewById(R.id.letterbox_education_dialog_title));
- assertEquals(mLayout.getBackgroundDim(), mLayout.getBackground());
+ assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground());
assertEquals(mLayout.getBackground().getAlpha(), 0);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
index 16517c0..14190f1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
@@ -56,6 +56,7 @@
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.compatui.DialogAnimationController;
import com.android.wm.shell.transition.Transitions;
import org.junit.After;
@@ -98,7 +99,7 @@
@Captor
private ArgumentCaptor<Runnable> mRunOnIdleCaptor;
- @Mock private LetterboxEduAnimationController mAnimationController;
+ @Mock private DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
@Mock private SyncTransactionQueue mSyncTransactionQueue;
@Mock private ShellTaskOrganizer.TaskListener mTaskListener;
@Mock private SurfaceControlViewHost mViewHost;
@@ -366,7 +367,7 @@
assertThat(params.width).isEqualTo(expectedWidth);
assertThat(params.height).isEqualTo(expectedHeight);
MarginLayoutParams dialogParams =
- (MarginLayoutParams) layout.getDialogContainer().getLayoutParams();
+ (MarginLayoutParams) layout.getDialogContainerView().getLayoutParams();
int verticalMargin = (int) mContext.getResources().getDimension(
R.dimen.letterbox_education_dialog_margin);
assertThat(dialogParams.topMargin).isEqualTo(verticalMargin + expectedExtraTopMargin);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index de2473b..9a92879 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -17,10 +17,18 @@
package com.android.wm.shell.desktopmode
import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.os.Binder
import android.testing.AndroidTestingRunner
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
import androidx.test.filters.SmallTest
@@ -29,6 +37,7 @@
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
@@ -37,9 +46,11 @@
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.After
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -79,6 +90,7 @@
desktopModeTaskRepository = DesktopModeTaskRepository()
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
+ whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
controller = createController()
@@ -221,6 +233,114 @@
assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED)
}
+ @Test
+ fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask = setUpFreeformTask()
+ markTaskVisible(freeformTask)
+ val fullscreenTask = createFullscreenTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+ assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask = setUpFreeformTask()
+ markTaskHidden(freeformTask)
+ val fullscreenTask = createFullscreenTask()
+ assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+ }
+
+ @Test
+ fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val fullscreenTask = createFullscreenTask()
+ assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+ }
+
+ @Test
+ fun handleRequest_freeformTask_freeformVisible_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask1 = setUpFreeformTask()
+ markTaskVisible(freeformTask1)
+
+ val freeformTask2 = createFreeformTask()
+ assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull()
+ }
+
+ @Test
+ fun handleRequest_freeformTask_freeformNotVisible_returnSwitchToFullscreenWCT() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask1 = setUpFreeformTask()
+ markTaskHidden(freeformTask1)
+
+ val freeformTask2 = createFreeformTask()
+ val result =
+ controller.handleRequest(
+ Binder(),
+ createTransition(freeformTask2, type = TRANSIT_TO_FRONT)
+ )
+ assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ }
+
+ @Test
+ fun handleRequest_freeformTask_noOtherTasks_returnSwitchToFullscreenWCT() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val task = createFreeformTask()
+ val result = controller.handleRequest(Binder(), createTransition(task))
+ assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ }
+
+ @Test
+ fun handleRequest_notOpenOrToFrontTransition_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val task =
+ TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .build()
+ val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE)
+ val result = controller.handleRequest(Binder(), transition)
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun handleRequest_noTriggerTask_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull()
+ }
+
+ @Test
+ fun handleRequest_triggerTaskNotStandard_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
+ assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
+ }
+
+ @Test
+ fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val task =
+ TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+ .build()
+ assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
+ }
+
private fun setUpFreeformTask(): RunningTaskInfo {
val task = createFreeformTask()
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
@@ -254,7 +374,7 @@
private fun getLatestWct(): WindowContainerTransaction {
val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ if (ENABLE_SHELL_TRANSITIONS) {
verify(transitions).startTransition(anyInt(), arg.capture(), isNull())
} else {
verify(shellTaskOrganizer).applyTransaction(arg.capture())
@@ -263,12 +383,19 @@
}
private fun verifyWCTNotExecuted() {
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ if (ENABLE_SHELL_TRANSITIONS) {
verify(transitions, never()).startTransition(anyInt(), any(), isNull())
} else {
verify(shellTaskOrganizer, never()).applyTransaction(any())
}
}
+
+ private fun createTransition(
+ task: RunningTaskInfo?,
+ @WindowManager.TransitionType type: Int = TRANSIT_OPEN
+ ): TransitionRequestInfo {
+ return TransitionRequestInfo(type, task, null /* remoteTransition */)
+ }
}
private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
index 4da47fd..db224be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
@@ -66,6 +66,8 @@
"com.android.settings.category.ia.battery_saver_settings";
public static final String CATEGORY_SMART_BATTERY_SETTINGS =
"com.android.settings.category.ia.smart_battery_settings";
+ public static final String CATEGORY_COMMUNAL_SETTINGS =
+ "com.android.settings.category.ia.communal";
public static final Map<String, String> KEY_COMPAT_MAP;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
index 340a6c7..c9dc1ba 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
@@ -60,8 +60,9 @@
allKeys.add(CategoryKey.CATEGORY_GESTURES);
allKeys.add(CategoryKey.CATEGORY_NIGHT_DISPLAY);
allKeys.add(CategoryKey.CATEGORY_SMART_BATTERY_SETTINGS);
+ allKeys.add(CategoryKey.CATEGORY_COMMUNAL_SETTINGS);
// DO NOT REMOVE ANYTHING ABOVE
- assertThat(allKeys.size()).isEqualTo(19);
+ assertThat(allKeys.size()).isEqualTo(20);
}
}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 25fb24a..6b4d5ae 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -31,6 +31,52 @@
],
}
+// Opt-in configuration for code depending on Jetpack Compose.
+soong_config_module_type {
+ name: "systemui_compose_java_defaults",
+ module_type: "java_defaults",
+ config_namespace: "ANDROID",
+ bool_variables: ["SYSTEMUI_USE_COMPOSE"],
+ properties: [
+ "srcs",
+ "static_libs",
+ ],
+}
+
+systemui_compose_java_defaults {
+ name: "SystemUI_compose_defaults",
+ soong_config_variables: {
+ SYSTEMUI_USE_COMPOSE: {
+ // Because files in compose/features/ depend on SystemUI
+ // code, we compile those files when compiling SystemUI-core.
+ // We also compile the ComposeFacade in
+ // compose/facade/enabled/.
+ srcs: [
+ "compose/features/src/**/*.kt",
+ "compose/facade/enabled/src/**/*.kt",
+ ],
+
+ // The dependencies needed by SystemUIComposeFeatures,
+ // except for SystemUI-core.
+ // Copied from compose/features/Android.bp.
+ static_libs: [
+ "SystemUIComposeCore",
+
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ "androidx.activity_activity-compose",
+ ],
+
+ // By default, Compose is disabled and we compile the ComposeFacade
+ // in compose/facade/disabled/.
+ conditions_default: {
+ srcs: ["compose/facade/disabled/src/**/*.kt"],
+ static_libs: [],
+ },
+ },
+ },
+}
+
java_library {
name: "SystemUI-proto",
@@ -68,6 +114,9 @@
android_library {
name: "SystemUI-core",
+ defaults: [
+ "SystemUI_compose_defaults",
+ ],
srcs: [
"src/**/*.kt",
"src/**/*.java",
@@ -227,6 +276,9 @@
android_library {
name: "SystemUI-tests",
+ defaults: [
+ "SystemUI_compose_defaults",
+ ],
manifest: "tests/AndroidManifest-base.xml",
additional_manifests: ["tests/AndroidManifest.xml"],
srcs: [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index f55fb97..9058510 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -169,11 +169,9 @@
setFloatUniform("in_progress", value)
val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
- setFloatUniform(
- "in_size",
- /* width= */ maxSize.x * curvedProg,
- /* height= */ maxSize.y * curvedProg
- )
+ currentWidth = maxSize.x * curvedProg
+ currentHeight = maxSize.y * curvedProg
+ setFloatUniform("in_size", /* width= */ currentWidth, /* height= */ currentHeight)
setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
// radius should not exceed width and height values.
setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
@@ -237,4 +235,10 @@
* False for a ring effect.
*/
var rippleFill: Boolean = false
+
+ var currentWidth: Float = 0f
+ private set
+
+ var currentHeight: Float = 0f
+ private set
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index ae28a8b..b37c734 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -36,7 +36,7 @@
*/
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
- private lateinit var rippleShader: RippleShader
+ protected lateinit var rippleShader: RippleShader
lateinit var rippleShape: RippleShape
private set
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt
new file mode 100644
index 0000000..3f2f96b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 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.compose.runtime
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.MovableContent
+import androidx.compose.runtime.currentComposer
+
+/**
+ * An overload of [androidx.compose.runtime.movableContentOf] with 5 parameters.
+ *
+ * @see androidx.compose.runtime.movableContentOf
+ */
+@OptIn(InternalComposeApi::class)
+fun <P1, P2, P3, P4, P5> movableContentOf(
+ content: @Composable (P1, P2, P3, P4, P5) -> Unit
+): @Composable (P1, P2, P3, P4, P5) -> Unit {
+ val movableContent =
+ MovableContent<Pair<Triple<P1, P2, P3>, Pair<P4, P5>>> {
+ content(
+ it.first.first,
+ it.first.second,
+ it.first.third,
+ it.second.first,
+ it.second.second,
+ )
+ }
+ return { p1, p2, p3, p4, p5 ->
+ currentComposer.insertMovableContent(movableContent, Triple(p1, p2, p3) to (p4 to p5))
+ }
+}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
new file mode 100644
index 0000000..6e728ce
--- /dev/null
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 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.compose
+
+import androidx.activity.ComponentActivity
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/** The Compose facade, when Compose is *not* available. */
+object ComposeFacade : BaseComposeFacade {
+ override fun isComposeAvailable(): Boolean = false
+
+ override fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ ) {
+ throwComposeUnavailableError()
+ }
+
+ private fun throwComposeUnavailableError() {
+ error(
+ "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
+ " other function on ComposeFacade."
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
new file mode 100644
index 0000000..16294d9
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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.compose
+
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.android.systemui.compose.theme.SystemUITheme
+import com.android.systemui.people.ui.compose.PeopleScreen
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/** The Compose facade, when Compose is available. */
+object ComposeFacade : BaseComposeFacade {
+ override fun isComposeAvailable(): Boolean = true
+
+ override fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ ) {
+ activity.setContent { SystemUITheme { PeopleScreen(viewModel, onResult) } }
+ }
+}
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 325ede6..4533330 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -35,6 +35,7 @@
"androidx.compose.runtime_runtime",
"androidx.compose.material3_material3",
+ "androidx.activity_activity-compose",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index efd683f..e4e0bd4 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -190,7 +190,6 @@
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/screenshot_dismiss_work_profile">
<ImageView
- android:id="@+id/screenshot_dismiss_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/overlay_dismiss_button_margin"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 2d756ae..f0cc42e 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -743,27 +743,6 @@
<!-- How long in milliseconds before full burn-in protection is achieved. -->
<integer name="config_dreamOverlayMillisUntilFullJitter">240000</integer>
- <!-- The duration in milliseconds of the y-translation animation when waking up from
- the dream -->
- <integer name="config_dreamOverlayOutTranslationYDurationMs">333</integer>
- <!-- The delay in milliseconds of the y-translation animation when waking up from
- the dream for the complications at the bottom of the screen -->
- <integer name="config_dreamOverlayOutTranslationYDelayBottomMs">33</integer>
- <!-- The delay in milliseconds of the y-translation animation when waking up from
- the dream for the complications at the top of the screen -->
- <integer name="config_dreamOverlayOutTranslationYDelayTopMs">117</integer>
- <!-- The duration in milliseconds of the alpha animation when waking up from the dream -->
- <integer name="config_dreamOverlayOutAlphaDurationMs">200</integer>
- <!-- The delay in milliseconds of the alpha animation when waking up from the dream for the
- complications at the top of the screen -->
- <integer name="config_dreamOverlayOutAlphaDelayTopMs">217</integer>
- <!-- The delay in milliseconds of the alpha animation when waking up from the dream for the
- complications at the bottom of the screen -->
- <integer name="config_dreamOverlayOutAlphaDelayBottomMs">133</integer>
- <!-- The duration in milliseconds of the blur animation when waking up from
- the dream -->
- <integer name="config_dreamOverlayOutBlurDurationMs">250</integer>
-
<integer name="complicationFadeOutMs">500</integer>
<integer name="complicationFadeInMs">500</integer>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 87d2c51..5848237 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1272,6 +1272,12 @@
translate into their final position. -->
<dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen>
+ <!-- DREAMING -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+ <dimen name="dreaming_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+
+ <!-- OCCLUDED -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+ <dimen name="occluded_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+
<!-- The amount of vertical offset for the keyguard during the full shade transition. -->
<dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 7a8ffa1..29369cd 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2751,7 +2751,7 @@
Error message shown when a button should be pressed and held to activate it, usually shown when
the user attempted to tap the button or held it for too short a time. [CHAR LIMIT=32].
-->
- <string name="keyguard_affordance_press_too_short">Press and hold to activate</string>
+ <string name="keyguard_affordance_press_too_short">Touch & hold to open</string>
<!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] -->
<string name="rear_display_bottom_sheet_cancel">Cancel</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index baaef19..7da27b1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -103,7 +103,6 @@
@Override
public void reset() {
- super.reset();
// start fresh
mDismissing = false;
mView.resetPasswordText(false /* animate */, false /* announce */);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index b143c5b..d1c9a30 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -121,7 +121,6 @@
@Override
public void reset() {
- mMessageAreaController.setMessage("", false);
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 8b9823b..b8e196f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -16,6 +16,8 @@
package com.android.keyguard;
+import static java.util.Collections.emptySet;
+
import android.content.Context;
import android.os.Trace;
import android.util.AttributeSet;
@@ -88,8 +90,9 @@
}
/** Sets a translationY value on every child view except for the media view. */
- public void setChildrenTranslationYExcludingMediaView(float translationY) {
- setChildrenTranslationYExcluding(translationY, Set.of(mMediaHostContainer));
+ public void setChildrenTranslationY(float translationY, boolean excludeMedia) {
+ setChildrenTranslationYExcluding(translationY,
+ excludeMedia ? Set.of(mMediaHostContainer) : emptySet());
}
/** Sets a translationY value on every view except for the views in the provided set. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 7849747..aec3063 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,8 @@
import android.util.Slog;
import com.android.keyguard.KeyguardClockSwitch.ClockSize;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -59,6 +61,7 @@
KeyguardUpdateMonitor keyguardUpdateMonitor,
ConfigurationController configurationController,
DozeParameters dozeParameters,
+ FeatureFlags featureFlags,
ScreenOffAnimationController screenOffAnimationController) {
super(keyguardStatusView);
mKeyguardSliceViewController = keyguardSliceViewController;
@@ -67,6 +70,8 @@
mConfigurationController = configurationController;
mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
dozeParameters, screenOffAnimationController, /* animateYPos= */ true);
+ mKeyguardVisibilityHelper.setOcclusionTransitionFlagEnabled(
+ featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION));
}
@Override
@@ -115,8 +120,8 @@
/**
* Sets a translationY on the views on the keyguard, except on the media view.
*/
- public void setTranslationYExcludingMedia(float translationY) {
- mView.setChildrenTranslationYExcludingMediaView(translationY);
+ public void setTranslationY(float translationY, boolean excludeMedia) {
+ mView.setChildrenTranslationY(translationY, excludeMedia);
}
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
index f974e27..edd150c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.view.ViewGroup
import com.android.systemui.R
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START
@@ -36,27 +38,29 @@
@Inject
constructor(
private val context: Context,
- unfoldProgressProvider: NaturalRotationUnfoldProgressProvider
+ statusBarStateController: StatusBarStateController,
+ unfoldProgressProvider: NaturalRotationUnfoldProgressProvider,
) {
/** Certain views only need to move if they are not currently centered */
var statusViewCentered = false
- private val filterSplitShadeOnly = { !statusViewCentered }
- private val filterNever = { true }
+ private val filterKeyguardAndSplitShadeOnly: () -> Boolean = {
+ statusBarStateController.getState() == KEYGUARD && !statusViewCentered }
+ private val filterKeyguard: () -> Boolean = { statusBarStateController.getState() == KEYGUARD }
private val translateAnimator by lazy {
UnfoldConstantTranslateAnimator(
viewsIdToTranslate =
setOf(
- ViewIdToTranslate(R.id.keyguard_status_area, START, filterNever),
+ ViewIdToTranslate(R.id.keyguard_status_area, START, filterKeyguard),
ViewIdToTranslate(
- R.id.lockscreen_clock_view_large, START, filterSplitShadeOnly),
- ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterNever),
+ R.id.lockscreen_clock_view_large, START, filterKeyguardAndSplitShadeOnly),
+ ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterKeyguard),
ViewIdToTranslate(
- R.id.notification_stack_scroller, END, filterSplitShadeOnly),
- ViewIdToTranslate(R.id.start_button, START, filterNever),
- ViewIdToTranslate(R.id.end_button, END, filterNever)),
+ R.id.notification_stack_scroller, END, filterKeyguardAndSplitShadeOnly),
+ ViewIdToTranslate(R.id.start_button, START, filterKeyguard),
+ ViewIdToTranslate(R.id.end_button, END, filterKeyguard)),
progressProvider = unfoldProgressProvider)
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 2c3d3a0..93027c1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -884,7 +884,7 @@
Assert.isMainThread();
if (mWakeOnFingerprintAcquiredStart && acquireInfo == FINGERPRINT_ACQUIRED_START) {
mPowerManager.wakeUp(
- SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC,
"com.android.systemui.keyguard:FINGERPRINT_ACQUIRED_START");
}
for (int i = 0; i < mCallbacks.size(); i++) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 498304b..bde0692 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -44,6 +44,7 @@
private boolean mAnimateYPos;
private boolean mKeyguardViewVisibilityAnimating;
private boolean mLastOccludedState = false;
+ private boolean mIsUnoccludeTransitionFlagEnabled = false;
private final AnimationProperties mAnimationProperties = new AnimationProperties();
public KeyguardVisibilityHelper(View view,
@@ -62,6 +63,10 @@
return mKeyguardViewVisibilityAnimating;
}
+ public void setOcclusionTransitionFlagEnabled(boolean enabled) {
+ mIsUnoccludeTransitionFlagEnabled = enabled;
+ }
+
/**
* Set the visibility of a keyguard view based on some new state.
*/
@@ -129,7 +134,7 @@
// since it may need to be cancelled due to keyguard lifecycle events.
mScreenOffAnimationController.animateInKeyguard(
mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
- } else if (mLastOccludedState && !isOccluded) {
+ } else if (!mIsUnoccludeTransitionFlagEnabled && mLastOccludedState && !isOccluded) {
// An activity was displayed over the lock screen, and has now gone away
mView.setVisibility(View.VISIBLE);
mView.setAlpha(0f);
@@ -142,7 +147,9 @@
.start();
} else {
mView.setVisibility(View.VISIBLE);
- mView.setAlpha(1f);
+ if (!mIsUnoccludeTransitionFlagEnabled) {
+ mView.setAlpha(1f);
+ }
}
} else {
mView.setVisibility(View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index 82e5704..805a20a 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -16,15 +16,21 @@
package com.android.systemui.clipboardoverlay;
+import static android.content.ClipDescription.CLASSIFICATION_COMPLETE;
+
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED;
import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED;
+import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN;
+
+import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.SystemProperties;
import android.provider.DeviceConfig;
+import android.provider.Settings;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -56,6 +62,7 @@
private final DeviceConfigProxy mDeviceConfig;
private final Provider<ClipboardOverlayController> mOverlayProvider;
private final ClipboardOverlayControllerLegacyFactory mOverlayFactory;
+ private final ClipboardToast mClipboardToast;
private final ClipboardManager mClipboardManager;
private final UiEventLogger mUiEventLogger;
private final FeatureFlags mFeatureFlags;
@@ -66,6 +73,7 @@
public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy,
Provider<ClipboardOverlayController> clipboardOverlayControllerProvider,
ClipboardOverlayControllerLegacyFactory overlayFactory,
+ ClipboardToast clipboardToast,
ClipboardManager clipboardManager,
UiEventLogger uiEventLogger,
FeatureFlags featureFlags) {
@@ -73,6 +81,7 @@
mDeviceConfig = deviceConfigProxy;
mOverlayProvider = clipboardOverlayControllerProvider;
mOverlayFactory = overlayFactory;
+ mClipboardToast = clipboardToast;
mClipboardManager = clipboardManager;
mUiEventLogger = uiEventLogger;
mFeatureFlags = featureFlags;
@@ -102,6 +111,15 @@
return;
}
+ if (!isUserSetupComplete()) {
+ // just show a toast, user should not access intents from this state
+ if (shouldShowToast(clipData)) {
+ mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource);
+ mClipboardToast.showCopiedToast();
+ }
+ return;
+ }
+
boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR);
if (mClipboardOverlay == null || enabled != mUsingNewOverlay) {
mUsingNewOverlay = enabled;
@@ -136,10 +154,26 @@
return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false);
}
+ boolean shouldShowToast(ClipData clipData) {
+ if (clipData == null) {
+ return false;
+ } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) {
+ // only show for classification complete if we aren't already showing a toast, to ignore
+ // the duplicate ClipData with classification
+ return !mClipboardToast.isShowing();
+ }
+ return true;
+ }
+
private static boolean isEmulator() {
return SystemProperties.getBoolean("ro.boot.qemu", false);
}
+ private boolean isUserSetupComplete() {
+ return Settings.Secure.getInt(mContext.getContentResolver(),
+ SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+ }
+
interface ClipboardOverlay {
void setClipData(ClipData clipData, String clipSource);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java
index 9917507..4b5f876 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java
@@ -43,7 +43,9 @@
@UiEvent(doc = "clipboard overlay tapped outside")
CLIPBOARD_OVERLAY_TAP_OUTSIDE(1077),
@UiEvent(doc = "clipboard overlay dismissed, miscellaneous reason")
- CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078);
+ CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078),
+ @UiEvent(doc = "clipboard toast shown")
+ CLIPBOARD_TOAST_SHOWN(1270);
private final int mId;
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java
new file mode 100644
index 0000000..0ed7d27
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 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.clipboardoverlay;
+
+import android.content.Context;
+import android.widget.Toast;
+
+import com.android.systemui.R;
+
+import javax.inject.Inject;
+
+/**
+ * Utility class for showing a simple clipboard toast on copy.
+ */
+class ClipboardToast extends Toast.Callback {
+ private final Context mContext;
+ private Toast mCopiedToast;
+
+ @Inject
+ ClipboardToast(Context context) {
+ mContext = context;
+ }
+
+ void showCopiedToast() {
+ if (mCopiedToast != null) {
+ mCopiedToast.cancel();
+ }
+ mCopiedToast = Toast.makeText(mContext,
+ R.string.clipboard_overlay_text_copied, Toast.LENGTH_SHORT);
+ mCopiedToast.show();
+ }
+
+ boolean isShowing() {
+ return mCopiedToast != null;
+ }
+
+ @Override // Toast.Callback
+ public void onToastHidden() {
+ super.onToastHidden();
+ mCopiedToast = null;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
new file mode 100644
index 0000000..e5ec727
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 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.compose
+
+import androidx.activity.ComponentActivity
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/**
+ * A facade to interact with Compose, when it is available.
+ *
+ * You should access this facade by calling the static methods on
+ * [com.android.systemui.compose.ComposeFacade] directly.
+ */
+interface BaseComposeFacade {
+ /**
+ * Whether Compose is currently available. This function should be checked before calling any
+ * other functions on this facade.
+ *
+ * This value will never change at runtime.
+ */
+ fun isComposeAvailable(): Boolean
+
+ /** Bind the content of [activity] to [viewModel]. */
+ fun setPeopleSpaceActivityContent(
+ activity: ComponentActivity,
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index f902252..2260e35 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -71,6 +71,7 @@
import android.os.PowerExemptionManager;
import android.os.PowerManager;
import android.os.ServiceManager;
+import android.os.SystemUpdateManager;
import android.os.UserManager;
import android.os.Vibrator;
import android.os.storage.StorageManager;
@@ -139,6 +140,12 @@
return context.getSystemService(AlarmManager.class);
}
+ @Provides
+ @Singleton
+ static Optional<SystemUpdateManager> provideSystemUpdateManager(Context context) {
+ return Optional.ofNullable(context.getSystemService(SystemUpdateManager.class));
+ }
+
/** */
@Provides
public AmbientDisplayConfiguration provideAmbientDisplayConfiguration(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt
index 16f4150..c746efd 100644
--- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt
@@ -20,7 +20,7 @@
import android.database.ContentObserver
import android.os.Handler
import android.os.Looper
-import android.provider.Settings
+import com.android.systemui.util.settings.GlobalSettings
/**
* Class to track the availability of [DemoMode]. Use this class to track the availability and
@@ -29,7 +29,10 @@
* This class works by wrapping a content observer for the relevant keys related to DemoMode
* availability and current on/off state, and triggering callbacks.
*/
-abstract class DemoModeAvailabilityTracker(val context: Context) {
+abstract class DemoModeAvailabilityTracker(
+ val context: Context,
+ val globalSettings: GlobalSettings,
+) {
var isInDemoMode = false
var isDemoModeAvailable = false
@@ -41,9 +44,9 @@
fun startTracking() {
val resolver = context.contentResolver
resolver.registerContentObserver(
- Settings.Global.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
+ globalSettings.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
resolver.registerContentObserver(
- Settings.Global.getUriFor(DEMO_MODE_ON), false, onObserver)
+ globalSettings.getUriFor(DEMO_MODE_ON), false, onObserver)
}
fun stopTracking() {
@@ -57,12 +60,11 @@
abstract fun onDemoModeFinished()
private fun checkIsDemoModeAllowed(): Boolean {
- return Settings.Global
- .getInt(context.contentResolver, DEMO_MODE_ALLOWED, 0) != 0
+ return globalSettings.getInt(DEMO_MODE_ALLOWED, 0) != 0
}
private fun checkIsDemoModeOn(): Boolean {
- return Settings.Global.getInt(context.contentResolver, DEMO_MODE_ON, 0) != 0
+ return globalSettings.getInt(DEMO_MODE_ON, 0) != 0
}
private val allowedObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
index 000bbe6..84f83f1 100644
--- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
+++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt
@@ -24,22 +24,28 @@
import android.os.UserHandle
import android.util.Log
import com.android.systemui.Dumpable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.demomode.DemoMode.ACTION_DEMO
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.util.Assert
import com.android.systemui.util.settings.GlobalSettings
import java.io.PrintWriter
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
/**
* Handles system broadcasts for [DemoMode]
*
* Injected via [DemoModeModule]
*/
-class DemoModeController constructor(
+class DemoModeController
+constructor(
private val context: Context,
private val dumpManager: DumpManager,
- private val globalSettings: GlobalSettings
+ private val globalSettings: GlobalSettings,
+ private val broadcastDispatcher: BroadcastDispatcher,
) : CallbackController<DemoMode>, Dumpable {
// Var updated when the availability tracker changes, or when we enter/exit demo mode in-process
@@ -58,9 +64,7 @@
requestFinishDemoMode()
val m = mutableMapOf<String, MutableList<DemoMode>>()
- DemoMode.COMMANDS.map { command ->
- m.put(command, mutableListOf())
- }
+ DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) }
receiverMap = m
}
@@ -71,7 +75,7 @@
initialized = true
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
// Due to DemoModeFragment running in systemui:tuner process, we have to observe for
// content changes to know if the setting turned on or off
@@ -81,8 +85,13 @@
val demoFilter = IntentFilter()
demoFilter.addAction(ACTION_DEMO)
- context.registerReceiverAsUser(broadcastReceiver, UserHandle.ALL, demoFilter,
- android.Manifest.permission.DUMP, null, Context.RECEIVER_EXPORTED)
+
+ broadcastDispatcher.registerReceiver(
+ receiver = broadcastReceiver,
+ filter = demoFilter,
+ user = UserHandle.ALL,
+ permission = android.Manifest.permission.DUMP,
+ )
}
override fun addCallback(listener: DemoMode) {
@@ -91,16 +100,15 @@
commands.forEach { command ->
if (!receiverMap.containsKey(command)) {
- throw IllegalStateException("Command ($command) not recognized. " +
- "See DemoMode.java for valid commands")
+ throw IllegalStateException(
+ "Command ($command) not recognized. " + "See DemoMode.java for valid commands"
+ )
}
receiverMap[command]!!.add(listener)
}
- synchronized(this) {
- receivers.add(listener)
- }
+ synchronized(this) { receivers.add(listener) }
if (isInDemoMode) {
listener.onDemoModeStarted()
@@ -109,14 +117,46 @@
override fun removeCallback(listener: DemoMode) {
synchronized(this) {
- listener.demoCommands().forEach { command ->
- receiverMap[command]!!.remove(listener)
- }
+ listener.demoCommands().forEach { command -> receiverMap[command]!!.remove(listener) }
receivers.remove(listener)
}
}
+ /**
+ * Create a [Flow] for the stream of demo mode arguments that come in for the given [command]
+ *
+ * This is equivalent of creating a listener manually and adding an event handler for the given
+ * command, like so:
+ *
+ * ```
+ * class Demoable {
+ * private val demoHandler = object : DemoMode {
+ * override fun demoCommands() = listOf(<command>)
+ *
+ * override fun dispatchDemoCommand(command: String, args: Bundle) {
+ * handleDemoCommand(args)
+ * }
+ * }
+ * }
+ * ```
+ *
+ * @param command The top-level demo mode command you want a stream for
+ */
+ fun demoFlowForCommand(command: String): Flow<Bundle> = conflatedCallbackFlow {
+ val callback =
+ object : DemoMode {
+ override fun demoCommands(): List<String> = listOf(command)
+
+ override fun dispatchDemoCommand(command: String, args: Bundle) {
+ trySend(args)
+ }
+ }
+
+ addCallback(callback)
+ awaitClose { removeCallback(callback) }
+ }
+
private fun setIsDemoModeAllowed(enabled: Boolean) {
// Turn off demo mode if it was on
if (isInDemoMode && !enabled) {
@@ -129,13 +169,9 @@
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
- synchronized(this) {
- copy = receivers.toList()
- }
+ synchronized(this) { copy = receivers.toList() }
- copy.forEach { r ->
- r.onDemoModeStarted()
- }
+ copy.forEach { r -> r.onDemoModeStarted() }
}
private fun exitDemoMode() {
@@ -143,18 +179,13 @@
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
- synchronized(this) {
- copy = receivers.toList()
- }
+ synchronized(this) { copy = receivers.toList() }
- copy.forEach { r ->
- r.onDemoModeFinished()
- }
+ copy.forEach { r -> r.onDemoModeFinished() }
}
fun dispatchDemoCommand(command: String, args: Bundle) {
Assert.isMainThread()
-
if (DEBUG) {
Log.d(TAG, "dispatchDemoCommand: $command, args=$args")
}
@@ -172,9 +203,7 @@
}
// See? demo mode is easy now, you just notify the listeners when their command is called
- receiverMap[command]!!.forEach { receiver ->
- receiver.dispatchDemoCommand(command, args)
- }
+ receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) }
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -183,65 +212,64 @@
pw.println(" isDemoModeAllowed=$isAvailable")
pw.print(" receivers=[")
val copy: List<DemoModeCommandReceiver>
- synchronized(this) {
- copy = receivers.toList()
- }
- copy.forEach { recv ->
- pw.print(" ${recv.javaClass.simpleName}")
- }
+ synchronized(this) { copy = receivers.toList() }
+ copy.forEach { recv -> pw.print(" ${recv.javaClass.simpleName}") }
pw.println(" ]")
pw.println(" receiverMap= [")
receiverMap.keys.forEach { command ->
pw.print(" $command : [")
- val recvs = receiverMap[command]!!.map { receiver ->
- receiver.javaClass.simpleName
- }.joinToString(",")
+ val recvs =
+ receiverMap[command]!!
+ .map { receiver -> receiver.javaClass.simpleName }
+ .joinToString(",")
pw.println("$recvs ]")
}
}
- private val tracker = object : DemoModeAvailabilityTracker(context) {
- override fun onDemoModeAvailabilityChanged() {
- setIsDemoModeAllowed(isDemoModeAvailable)
- }
+ private val tracker =
+ object : DemoModeAvailabilityTracker(context, globalSettings) {
+ override fun onDemoModeAvailabilityChanged() {
+ setIsDemoModeAllowed(isDemoModeAvailable)
+ }
- override fun onDemoModeStarted() {
- if (this@DemoModeController.isInDemoMode != isInDemoMode) {
- enterDemoMode()
+ override fun onDemoModeStarted() {
+ if (this@DemoModeController.isInDemoMode != isInDemoMode) {
+ enterDemoMode()
+ }
+ }
+
+ override fun onDemoModeFinished() {
+ if (this@DemoModeController.isInDemoMode != isInDemoMode) {
+ exitDemoMode()
+ }
}
}
- override fun onDemoModeFinished() {
- if (this@DemoModeController.isInDemoMode != isInDemoMode) {
- exitDemoMode()
+ private val broadcastReceiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (DEBUG) {
+ Log.v(TAG, "onReceive: $intent")
+ }
+
+ val action = intent.action
+ if (!ACTION_DEMO.equals(action)) {
+ return
+ }
+
+ val bundle = intent.extras ?: return
+ val command = bundle.getString("command", "").trim().lowercase()
+ if (command.isEmpty()) {
+ return
+ }
+
+ try {
+ dispatchDemoCommand(command, bundle)
+ } catch (t: Throwable) {
+ Log.w(TAG, "Error running demo command, intent=$intent $t")
+ }
}
}
- }
-
- private val broadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- if (DEBUG) {
- Log.v(TAG, "onReceive: $intent")
- }
-
- val action = intent.action
- if (!ACTION_DEMO.equals(action)) {
- return
- }
-
- val bundle = intent.extras ?: return
- val command = bundle.getString("command", "").trim().toLowerCase()
- if (command.length == 0) {
- return
- }
-
- try {
- dispatchDemoCommand(command, bundle)
- } catch (t: Throwable) {
- Log.w(TAG, "Error running demo command, intent=$intent $t")
- }
- }
- }
fun requestSetDemoModeAllowed(allowed: Boolean) {
setGlobal(DEMO_MODE_ALLOWED, if (allowed) 1 else 0)
@@ -258,10 +286,12 @@
private fun setGlobal(key: String, value: Int) {
globalSettings.putInt(key, value)
}
+
+ companion object {
+ const val DEMO_MODE_ALLOWED = "sysui_demo_allowed"
+ const val DEMO_MODE_ON = "sysui_tuner_demo_on"
+ }
}
private const val TAG = "DemoModeController"
-private const val DEMO_MODE_ALLOWED = "sysui_demo_allowed"
-private const val DEMO_MODE_ON = "sysui_tuner_demo_on"
-
private const val DEBUG = false
diff --git a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java
index de9affb..b84fa5a 100644
--- a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java
@@ -18,6 +18,7 @@
import android.content.Context;
+import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
@@ -37,8 +38,14 @@
static DemoModeController provideDemoModeController(
Context context,
DumpManager dumpManager,
- GlobalSettings globalSettings) {
- DemoModeController dmc = new DemoModeController(context, dumpManager, globalSettings);
+ GlobalSettings globalSettings,
+ BroadcastDispatcher broadcastDispatcher
+ ) {
+ DemoModeController dmc = new DemoModeController(
+ context,
+ dumpManager,
+ globalSettings,
+ broadcastDispatcher);
dmc.initialize();
return /*run*/dmc;
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 5d21349..5b90ef2 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -16,7 +16,14 @@
package com.android.systemui.doze;
+import static android.os.PowerManager.WAKE_REASON_BIOMETRIC;
+import static android.os.PowerManager.WAKE_REASON_GESTURE;
+import static android.os.PowerManager.WAKE_REASON_LIFT;
+import static android.os.PowerManager.WAKE_REASON_PLUGGED_IN;
+import static android.os.PowerManager.WAKE_REASON_TAP;
+
import android.annotation.IntDef;
+import android.os.PowerManager;
import android.util.TimeUtils;
import androidx.annotation.NonNull;
@@ -511,6 +518,25 @@
}
}
+ /**
+ * Converts {@link Reason} to {@link PowerManager.WakeReason}.
+ */
+ public static @PowerManager.WakeReason int getPowerManagerWakeReason(@Reason int wakeReason) {
+ switch (wakeReason) {
+ case REASON_SENSOR_DOUBLE_TAP:
+ case REASON_SENSOR_TAP:
+ return WAKE_REASON_TAP;
+ case REASON_SENSOR_PICKUP:
+ return WAKE_REASON_LIFT;
+ case REASON_SENSOR_UDFPS_LONG_PRESS:
+ return WAKE_REASON_BIOMETRIC;
+ case PULSE_REASON_DOCKING:
+ return WAKE_REASON_PLUGGED_IN;
+ default:
+ return WAKE_REASON_GESTURE;
+ }
+ }
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({PULSE_REASON_NONE, PULSE_REASON_INTENT, PULSE_REASON_NOTIFICATION,
PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 833ff3f..37183e8 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -22,6 +22,7 @@
import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN;
import android.annotation.AnyThread;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorManager;
@@ -40,6 +41,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.R;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
@@ -140,6 +142,7 @@
}
DozeSensors(
+ Resources resources,
AsyncSensorManager sensorManager,
DozeParameters dozeParameters,
AmbientDisplayConfiguration config,
@@ -185,7 +188,8 @@
new TriggerSensor(
mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
Settings.Secure.DOZE_PICK_UP_GESTURE,
- true /* settingDef */,
+ resources.getBoolean(
+ R.bool.config_dozePickupGestureEnabled) /* settingDef */,
config.dozePickupSensorAvailable(),
DozeLog.REASON_SENSOR_PICKUP, false /* touchCoords */,
false /* touchscreen */,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index f8bd1e7..ba38ab0 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -116,8 +116,8 @@
@Override
public void requestWakeUp(@DozeLog.Reason int reason) {
- PowerManager pm = getSystemService(PowerManager.class);
- pm.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ final PowerManager pm = getSystemService(PowerManager.class);
+ pm.wakeUp(SystemClock.uptimeMillis(), DozeLog.getPowerManagerWakeReason(reason),
"com.android.systemui:NODOZE " + DozeLog.reasonToString(reason));
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 3f9f14c..b95c3f3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -210,7 +210,7 @@
mAllowPulseTriggers = true;
mSessionTracker = sessionTracker;
- mDozeSensors = new DozeSensors(mSensorManager, dozeParameters,
+ mDozeSensors = new DozeSensors(mContext.getResources(), mSensorManager, dozeParameters,
config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor,
secureSettings, authController, devicePostureController, userTracker);
mDockManager = dockManager;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index 9b8ef71..c882f8a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -22,6 +22,9 @@
import android.view.View
import android.view.animation.Interpolator
import androidx.core.animation.doOnEnd
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.dreams.complication.ComplicationHostViewController
import com.android.systemui.dreams.complication.ComplicationLayoutParams
@@ -29,10 +32,20 @@
import com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP
import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position
import com.android.systemui.dreams.dagger.DreamOverlayModule
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_ANIMATION_DURATION
+import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.CrossFadeHelper
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import com.android.systemui.util.concurrency.DelayableExecutor
import javax.inject.Inject
import javax.inject.Named
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.launch
/** Controller for dream overlay animations. */
class DreamOverlayAnimationsController
@@ -43,6 +56,8 @@
private val mStatusBarViewController: DreamOverlayStatusBarViewController,
private val mOverlayStateController: DreamOverlayStateController,
@Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int,
+ private val transitionViewModel: DreamingToLockscreenTransitionViewModel,
+ private val configController: ConfigurationController,
@Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION)
private val mDreamInBlurAnimDurationMs: Long,
@Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION)
@@ -51,22 +66,10 @@
private val mDreamInTranslationYDistance: Int,
@Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION)
private val mDreamInTranslationYDurationMs: Long,
- @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DISTANCE)
- private val mDreamOutTranslationYDistance: Int,
- @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DURATION)
- private val mDreamOutTranslationYDurationMs: Long,
- @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM)
- private val mDreamOutTranslationYDelayBottomMs: Long,
- @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DELAY_TOP)
- private val mDreamOutTranslationYDelayTopMs: Long,
- @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DURATION) private val mDreamOutAlphaDurationMs: Long,
- @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DELAY_BOTTOM)
- private val mDreamOutAlphaDelayBottomMs: Long,
- @Named(DreamOverlayModule.DREAM_OUT_ALPHA_DELAY_TOP) private val mDreamOutAlphaDelayTopMs: Long,
- @Named(DreamOverlayModule.DREAM_OUT_BLUR_DURATION) private val mDreamOutBlurDurationMs: Long
) {
private var mAnimator: Animator? = null
+ private lateinit var view: View
/**
* Store the current alphas at the various positions. This is so that we may resume an animation
@@ -76,9 +79,61 @@
private var mCurrentBlurRadius: Float = 0f
+ fun init(view: View) {
+ this.view = view
+
+ view.repeatWhenAttached {
+ val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
+ val configCallback =
+ object : ConfigurationListener {
+ override fun onDensityOrFontScaleChanged() {
+ configurationBasedDimensions.value = loadFromResources(view)
+ }
+ }
+
+ configController.addCallback(configCallback)
+
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ /* Translation animations, when moving from DREAMING->LOCKSCREEN state */
+ launch {
+ configurationBasedDimensions
+ .flatMapLatest {
+ transitionViewModel.dreamOverlayTranslationY(it.translationYPx)
+ }
+ .collect { px ->
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsTranslationYAtPosition(px, position)
+ },
+ POSITION_TOP or POSITION_BOTTOM
+ )
+ }
+ }
+
+ /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */
+ launch {
+ transitionViewModel.dreamOverlayAlpha.collect { alpha ->
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsAlphaAtPosition(
+ alpha = alpha,
+ position = position,
+ fadingOut = true,
+ )
+ },
+ POSITION_TOP or POSITION_BOTTOM
+ )
+ }
+ }
+ }
+
+ configController.removeCallback(configCallback)
+ }
+ }
+
/** Starts the dream content and dream overlay entry animations. */
@JvmOverloads
- fun startEntryAnimations(view: View, animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) {
+ fun startEntryAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) {
cancelAnimations()
mAnimator =
@@ -113,73 +168,9 @@
}
/** Starts the dream content and dream overlay exit animations. */
- @JvmOverloads
- fun startExitAnimations(
- view: View,
- doneCallback: () -> Unit,
- animatorBuilder: () -> AnimatorSet = { AnimatorSet() }
- ) {
+ fun wakeUp(doneCallback: Runnable, executor: DelayableExecutor) {
cancelAnimations()
-
- mAnimator =
- animatorBuilder().apply {
- playTogether(
- blurAnimator(
- view = view,
- // Start the blurring wherever the entry animation ended, in
- // case it was cancelled early.
- fromBlurRadius = mCurrentBlurRadius,
- toBlurRadius = mDreamBlurRadius.toFloat(),
- durationMs = mDreamOutBlurDurationMs,
- interpolator = Interpolators.EMPHASIZED_ACCELERATE
- ),
- translationYAnimator(
- from = 0f,
- to = mDreamOutTranslationYDistance.toFloat(),
- durationMs = mDreamOutTranslationYDurationMs,
- delayMs = mDreamOutTranslationYDelayBottomMs,
- positions = POSITION_BOTTOM,
- interpolator = Interpolators.EMPHASIZED_ACCELERATE
- ),
- translationYAnimator(
- from = 0f,
- to = mDreamOutTranslationYDistance.toFloat(),
- durationMs = mDreamOutTranslationYDurationMs,
- delayMs = mDreamOutTranslationYDelayTopMs,
- positions = POSITION_TOP,
- interpolator = Interpolators.EMPHASIZED_ACCELERATE
- ),
- alphaAnimator(
- from =
- mCurrentAlphaAtPosition.getOrDefault(
- key = POSITION_BOTTOM,
- defaultValue = 1f
- ),
- to = 0f,
- durationMs = mDreamOutAlphaDurationMs,
- delayMs = mDreamOutAlphaDelayBottomMs,
- positions = POSITION_BOTTOM
- ),
- alphaAnimator(
- from =
- mCurrentAlphaAtPosition.getOrDefault(
- key = POSITION_TOP,
- defaultValue = 1f
- ),
- to = 0f,
- durationMs = mDreamOutAlphaDurationMs,
- delayMs = mDreamOutAlphaDelayTopMs,
- positions = POSITION_TOP
- )
- )
- doOnEnd {
- mAnimator = null
- mOverlayStateController.setExitAnimationsRunning(false)
- doneCallback()
- }
- start()
- }
- mOverlayStateController.setExitAnimationsRunning(true)
+ executor.executeDelayed(doneCallback, DREAM_ANIMATION_DURATION.inWholeMilliseconds)
}
/** Cancels the dream content and dream overlay animations, if they're currently running. */
@@ -288,4 +279,15 @@
mStatusBarViewController.setTranslationY(translationY)
}
}
+
+ private fun loadFromResources(view: View): ConfigurationBasedDimensions {
+ return ConfigurationBasedDimensions(
+ translationYPx =
+ view.resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset),
+ )
+ }
+
+ private data class ConfigurationBasedDimensions(
+ val translationYPx: Int,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
new file mode 100644
index 0000000..d5ff8f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.CallbackController
+import javax.inject.Inject
+
+/** Dream overlay-related callback information */
+@SysUISingleton
+class DreamOverlayCallbackController @Inject constructor() :
+ CallbackController<DreamOverlayCallbackController.Callback> {
+
+ private val callbacks = mutableSetOf<DreamOverlayCallbackController.Callback>()
+
+ var isDreaming = false
+ private set
+
+ override fun addCallback(callback: DreamOverlayCallbackController.Callback) {
+ callbacks.add(callback)
+ }
+
+ override fun removeCallback(callback: DreamOverlayCallbackController.Callback) {
+ callbacks.remove(callback)
+ }
+
+ fun onWakeUp() {
+ isDreaming = false
+ callbacks.forEach { it.onWakeUp() }
+ }
+
+ fun onStartDream() {
+ isDreaming = true
+ callbacks.forEach { it.onStartDream() }
+ }
+
+ interface Callback {
+ /** Dream overlay has ended */
+ fun onWakeUp()
+
+ /** Dream overlay has started */
+ fun onStartDream()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 9d7ad30..3106173 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -42,9 +42,9 @@
import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.util.ViewController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
import java.util.Arrays;
-import java.util.concurrent.Executor;
import javax.inject.Inject;
import javax.inject.Named;
@@ -170,6 +170,7 @@
protected void onInit() {
mStatusBarViewController.init();
mComplicationHostViewController.init();
+ mDreamOverlayAnimationsController.init(mView);
}
@Override
@@ -184,7 +185,7 @@
// Start dream entry animations. Skip animations for low light clock.
if (!mStateController.isLowLightActive()) {
- mDreamOverlayAnimationsController.startEntryAnimations(mView);
+ mDreamOverlayAnimationsController.startEntryAnimations();
}
}
@@ -261,10 +262,8 @@
* @param onAnimationEnd Callback to trigger once animations are finished.
* @param callbackExecutor Executor to execute the callback on.
*/
- public void wakeUp(@NonNull Runnable onAnimationEnd, @NonNull Executor callbackExecutor) {
- mDreamOverlayAnimationsController.startExitAnimations(mView, () -> {
- callbackExecutor.execute(onAnimationEnd);
- return null;
- });
+ public void wakeUp(@NonNull Runnable onAnimationEnd,
+ @NonNull DelayableExecutor callbackExecutor) {
+ mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index e76d5b3..a9a9cae 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -43,8 +43,7 @@
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
-
-import java.util.concurrent.Executor;
+import com.android.systemui.util.concurrency.DelayableExecutor;
import javax.inject.Inject;
import javax.inject.Named;
@@ -61,10 +60,11 @@
// The Context is used to construct the hosting constraint layout and child overlay views.
private final Context mContext;
// The Executor ensures actions and ui updates happen on the same thread.
- private final Executor mExecutor;
+ private final DelayableExecutor mExecutor;
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
+ private final DreamOverlayCallbackController mDreamOverlayCallbackController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Nullable
private final ComponentName mLowLightDreamComponent;
@@ -126,14 +126,15 @@
@Inject
public DreamOverlayService(
Context context,
- @Main Executor executor,
+ @Main DelayableExecutor executor,
WindowManager windowManager,
DreamOverlayComponent.Factory dreamOverlayComponentFactory,
DreamOverlayStateController stateController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
UiEventLogger uiEventLogger,
@Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
- ComponentName lowLightDreamComponent) {
+ ComponentName lowLightDreamComponent,
+ DreamOverlayCallbackController dreamOverlayCallbackController) {
mContext = context;
mExecutor = executor;
mWindowManager = windowManager;
@@ -142,6 +143,7 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
mStateController = stateController;
mUiEventLogger = uiEventLogger;
+ mDreamOverlayCallbackController = dreamOverlayCallbackController;
final ViewModelStore viewModelStore = new ViewModelStore();
final Complication.Host host =
@@ -201,6 +203,7 @@
dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
+ mDreamOverlayCallbackController.onStartDream();
mStarted = true;
});
}
@@ -217,6 +220,7 @@
public void onWakeUp(@NonNull Runnable onCompletedCallback) {
mExecutor.execute(() -> {
if (mDreamOverlayContainerViewController != null) {
+ mDreamOverlayCallbackController.onWakeUp();
mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor);
}
});
@@ -226,6 +230,7 @@
* 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
* set by the {@link android.service.dreams.DreamService} on the dream Window.
+ *
* @param layoutParams The {@link android.view.WindowManager.LayoutParams} which allow inserting
* into the dream window.
*/
@@ -272,7 +277,11 @@
private void resetCurrentDreamOverlayLocked() {
if (mStarted && mWindow != null) {
- mWindowManager.removeView(mWindow.getDecorView());
+ try {
+ mWindowManager.removeView(mWindow.getDecorView());
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Error removing decor view when resetting overlay", e);
+ }
}
mStateController.setOverlayActive(false);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index 4f1ac1a..0d58a69 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -55,22 +55,6 @@
"dream_in_complications_translation_y";
public static final String DREAM_IN_TRANSLATION_Y_DURATION =
"dream_in_complications_translation_y_duration";
- public static final String DREAM_OUT_TRANSLATION_Y_DISTANCE =
- "dream_out_complications_translation_y";
- public static final String DREAM_OUT_TRANSLATION_Y_DURATION =
- "dream_out_complications_translation_y_duration";
- public static final String DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM =
- "dream_out_complications_translation_y_delay_bottom";
- public static final String DREAM_OUT_TRANSLATION_Y_DELAY_TOP =
- "dream_out_complications_translation_y_delay_top";
- public static final String DREAM_OUT_ALPHA_DURATION =
- "dream_out_complications_alpha_duration";
- public static final String DREAM_OUT_ALPHA_DELAY_BOTTOM =
- "dream_out_complications_alpha_delay_bottom";
- public static final String DREAM_OUT_ALPHA_DELAY_TOP =
- "dream_out_complications_alpha_delay_top";
- public static final String DREAM_OUT_BLUR_DURATION =
- "dream_out_blur_duration";
/** */
@Provides
@@ -185,66 +169,6 @@
return (long) resources.getInteger(R.integer.config_dreamOverlayInTranslationYDurationMs);
}
- /**
- * Provides the number of pixels to translate complications when waking up from dream.
- */
- @Provides
- @Named(DREAM_OUT_TRANSLATION_Y_DISTANCE)
- @DreamOverlayComponent.DreamOverlayScope
- static int providesDreamOutComplicationsTranslationY(@Main Resources resources) {
- return resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset);
- }
-
- @Provides
- @Named(DREAM_OUT_TRANSLATION_Y_DURATION)
- @DreamOverlayComponent.DreamOverlayScope
- static long providesDreamOutComplicationsTranslationYDuration(@Main Resources resources) {
- return (long) resources.getInteger(R.integer.config_dreamOverlayOutTranslationYDurationMs);
- }
-
- @Provides
- @Named(DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM)
- @DreamOverlayComponent.DreamOverlayScope
- static long providesDreamOutComplicationsTranslationYDelayBottom(@Main Resources resources) {
- return (long) resources.getInteger(
- R.integer.config_dreamOverlayOutTranslationYDelayBottomMs);
- }
-
- @Provides
- @Named(DREAM_OUT_TRANSLATION_Y_DELAY_TOP)
- @DreamOverlayComponent.DreamOverlayScope
- static long providesDreamOutComplicationsTranslationYDelayTop(@Main Resources resources) {
- return (long) resources.getInteger(R.integer.config_dreamOverlayOutTranslationYDelayTopMs);
- }
-
- @Provides
- @Named(DREAM_OUT_ALPHA_DURATION)
- @DreamOverlayComponent.DreamOverlayScope
- static long providesDreamOutComplicationsAlphaDuration(@Main Resources resources) {
- return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDurationMs);
- }
-
- @Provides
- @Named(DREAM_OUT_ALPHA_DELAY_BOTTOM)
- @DreamOverlayComponent.DreamOverlayScope
- static long providesDreamOutComplicationsAlphaDelayBottom(@Main Resources resources) {
- return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDelayBottomMs);
- }
-
- @Provides
- @Named(DREAM_OUT_ALPHA_DELAY_TOP)
- @DreamOverlayComponent.DreamOverlayScope
- static long providesDreamOutComplicationsAlphaDelayTop(@Main Resources resources) {
- return (long) resources.getInteger(R.integer.config_dreamOverlayOutAlphaDelayTopMs);
- }
-
- @Provides
- @Named(DREAM_OUT_BLUR_DURATION)
- @DreamOverlayComponent.DreamOverlayScope
- static long providesDreamOutBlurDuration(@Main Resources resources) {
- return (long) resources.getInteger(R.integer.config_dreamOverlayOutBlurDurationMs);
- }
-
@Provides
@DreamOverlayComponent.DreamOverlayScope
static LifecycleOwner providesLifecycleOwner(Lazy<LifecycleRegistry> lifecycleRegistryLazy) {
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index c982131..276a290 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -51,6 +51,11 @@
registerDumpable(name, module, DumpPriority.CRITICAL)
}
+ /** See [registerNormalDumpable]. */
+ fun registerNormalDumpable(module: Dumpable) {
+ registerNormalDumpable(module::class.java.simpleName, module)
+ }
+
/**
* Registers a dumpable to be called during the NORMAL section of the bug report.
*
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 339fd13..dbeef8d 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -181,10 +181,6 @@
@JvmField
val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false)
- // TODO(b/262780002): Tracking Bug
- @JvmField
- val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
-
/** Flag to control the migration of face auth to modern architecture. */
// TODO(b/262838215): Tracking bug
@JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor")
@@ -193,6 +189,15 @@
// TODO(b/244313043): Tracking bug
@JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
+ // TODO(b/262780002): Tracking Bug
+ @JvmField
+ val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
+
+ /** A different path for unocclusion transitions back to keyguard */
+ // TODO(b/262859270): Tracking Bug
+ @JvmField
+ val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -310,12 +315,18 @@
// TODO(b/261734857): Tracking Bug
@JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
+ // TODO(b/263272731): Tracking Bug
+ val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
+ unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
// TODO(b/254512758): Tracking Bug
@JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple")
+ val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot")
+
// 1100 - windowing
@Keep
@JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d231870..8aada1f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -33,6 +33,7 @@
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
import static com.android.systemui.DejankUtils.whitelistIpcs;
+import static com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.LOCKSCREEN_ANIMATION_DURATION_MS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -1231,8 +1232,7 @@
mDreamOpenAnimationDuration = context.getResources().getInteger(
com.android.internal.R.integer.config_dreamOpenAnimationDuration);
- mDreamCloseAnimationDuration = context.getResources().getInteger(
- com.android.internal.R.integer.config_dreamCloseAnimationDuration);
+ mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS;
}
public void userActivity() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 148792b..a4fd087 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -29,6 +29,7 @@
import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
import com.android.systemui.doze.DozeTransitionListener
+import com.android.systemui.dreams.DreamOverlayCallbackController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -78,6 +79,9 @@
*/
val isKeyguardShowing: Flow<Boolean>
+ /** Is an activity showing over the keyguard? */
+ val isKeyguardOccluded: Flow<Boolean>
+
/** Observable for the signal that keyguard is about to go away. */
val isKeyguardGoingAway: Flow<Boolean>
@@ -104,6 +108,9 @@
*/
val isDreaming: Flow<Boolean>
+ /** Observable for whether the device is dreaming with an overlay, see [DreamOverlayService] */
+ val isDreamingWithOverlay: Flow<Boolean>
+
/**
* Observable for the amount of doze we are currently in.
*
@@ -176,6 +183,7 @@
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val dozeTransitionListener: DozeTransitionListener,
private val authController: AuthController,
+ private val dreamOverlayCallbackController: DreamOverlayCallbackController,
) : KeyguardRepository {
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
override val animateBottomAreaDozingTransitions =
@@ -187,28 +195,55 @@
private val _clockPosition = MutableStateFlow(Position(0, 0))
override val clockPosition = _clockPosition.asStateFlow()
- override val isKeyguardShowing: Flow<Boolean> = conflatedCallbackFlow {
- val callback =
- object : KeyguardStateController.Callback {
- override fun onKeyguardShowingChanged() {
- trySendWithFailureLogging(
- keyguardStateController.isShowing,
- TAG,
- "updated isKeyguardShowing"
- )
- }
+ override val isKeyguardShowing: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardStateController.Callback {
+ override fun onKeyguardShowingChanged() {
+ trySendWithFailureLogging(
+ keyguardStateController.isShowing,
+ TAG,
+ "updated isKeyguardShowing"
+ )
+ }
+ }
+
+ keyguardStateController.addCallback(callback)
+ // Adding the callback does not send an initial update.
+ trySendWithFailureLogging(
+ keyguardStateController.isShowing,
+ TAG,
+ "initial isKeyguardShowing"
+ )
+
+ awaitClose { keyguardStateController.removeCallback(callback) }
}
+ .distinctUntilChanged()
- keyguardStateController.addCallback(callback)
- // Adding the callback does not send an initial update.
- trySendWithFailureLogging(
- keyguardStateController.isShowing,
- TAG,
- "initial isKeyguardShowing"
- )
+ override val isKeyguardOccluded: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardStateController.Callback {
+ override fun onKeyguardShowingChanged() {
+ trySendWithFailureLogging(
+ keyguardStateController.isOccluded,
+ TAG,
+ "updated isKeyguardOccluded"
+ )
+ }
+ }
- awaitClose { keyguardStateController.removeCallback(callback) }
- }
+ keyguardStateController.addCallback(callback)
+ // Adding the callback does not send an initial update.
+ trySendWithFailureLogging(
+ keyguardStateController.isOccluded,
+ TAG,
+ "initial isKeyguardOccluded"
+ )
+
+ awaitClose { keyguardStateController.removeCallback(callback) }
+ }
+ .distinctUntilChanged()
override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
val callback =
@@ -275,6 +310,28 @@
}
.distinctUntilChanged()
+ override val isDreamingWithOverlay: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : DreamOverlayCallbackController.Callback {
+ override fun onStartDream() {
+ trySendWithFailureLogging(true, TAG, "updated isDreamingWithOverlay")
+ }
+ override fun onWakeUp() {
+ trySendWithFailureLogging(false, TAG, "updated isDreamingWithOverlay")
+ }
+ }
+ dreamOverlayCallbackController.addCallback(callback)
+ trySendWithFailureLogging(
+ dreamOverlayCallbackController.isDreaming,
+ TAG,
+ "initial isDreamingWithOverlay",
+ )
+
+ awaitClose { dreamOverlayCallbackController.removeCallback(callback) }
+ }
+ .distinctUntilChanged()
+
override val isDreaming: Flow<Boolean> =
conflatedCallbackFlow {
val callback =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 5bb586e..343c2dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -66,8 +66,8 @@
}
/**
- * Begin a transition from one state to another. Will not start if another transition is in
- * progress.
+ * Begin a transition from one state to another. Transitions are interruptible, and will issue a
+ * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one.
*/
fun startTransition(info: TransitionInfo): UUID?
@@ -131,6 +131,10 @@
}
override fun startTransition(info: TransitionInfo): UUID? {
+ if (lastStep.from == info.from && lastStep.to == info.to) {
+ Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
+ return null
+ }
if (lastStep.transitionState != TransitionState.FINISHED) {
Log.i(TAG, "Transition still active: $lastStep, canceling")
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
deleted file mode 100644
index dad166f..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.keyguard.domain.interactor
-
-import android.animation.ValueAnimator
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionInfo
-import com.android.systemui.util.kotlin.sample
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class AodToGoneTransitionInteractor
-@Inject
-constructor(
- @Application private val scope: CoroutineScope,
- private val keyguardInteractor: KeyguardInteractor,
- private val keyguardTransitionRepository: KeyguardTransitionRepository,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodToGoneTransitionInteractor::class.simpleName!!) {
-
- override fun start() {
- scope.launch {
- keyguardInteractor.biometricUnlockState
- .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
- .collect { pair ->
- val (biometricUnlockState, keyguardState) = pair
- if (
- keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- name,
- KeyguardState.AOD,
- KeyguardState.GONE,
- getAnimator(),
- )
- )
- }
- }
- }
- }
-
- private fun getAnimator(): ValueAnimator {
- return ValueAnimator().apply {
- setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
- }
- }
-
- companion object {
- private const val TRANSITION_DURATION_MS = 500L
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
similarity index 76%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index f3d2905..c2d139c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -30,33 +31,33 @@
import kotlinx.coroutines.launch
@SysUISingleton
-class AodLockscreenTransitionInteractor
+class FromAodTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodLockscreenTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromAodTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForTransitionToAodFromLockscreen()
- listenForTransitionToLockscreenFromDozeStates()
+ listenForAodToLockscreen()
+ listenForAodToGone()
}
- private fun listenForTransitionToAodFromLockscreen() {
+ private fun listenForAodToLockscreen() {
scope.launch {
keyguardInteractor
- .dozeTransitionTo(DozeStateModel.DOZE_AOD)
+ .dozeTransitionTo(DozeStateModel.FINISH)
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
val (dozeToAod, lastStartedStep) = pair
- if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ if (lastStartedStep.to == KeyguardState.AOD) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- KeyguardState.LOCKSCREEN,
KeyguardState.AOD,
+ KeyguardState.LOCKSCREEN,
getAnimator(),
)
)
@@ -65,20 +66,20 @@
}
}
- private fun listenForTransitionToLockscreenFromDozeStates() {
- val canGoToLockscreen = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+ private fun listenForAodToGone() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.FINISH)
- .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ keyguardInteractor.biometricUnlockState
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (canGoToLockscreen.contains(lastStartedStep.to)) {
+ val (biometricUnlockState, keyguardState) = pair
+ if (
+ keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)
+ ) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- lastStartedStep.to,
- KeyguardState.LOCKSCREEN,
+ KeyguardState.AOD,
+ KeyguardState.GONE,
getAnimator(),
)
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
index 056c44d..0e9c447 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
@@ -23,16 +23,18 @@
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.util.kotlin.sample
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@SysUISingleton
-class BouncerToGoneTransitionInteractor
+class FromBouncerTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
@@ -40,15 +42,54 @@
private val shadeRepository: ShadeRepository,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor
-) : TransitionInteractor(BouncerToGoneTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromBouncerTransitionInteractor::class.simpleName!!) {
private var transitionId: UUID? = null
override fun start() {
- listenForKeyguardGoingAway()
+ listenForBouncerToGone()
+ listenForBouncerToLockscreenOrAod()
}
- private fun listenForKeyguardGoingAway() {
+ private fun listenForBouncerToLockscreenOrAod() {
+ scope.launch {
+ keyguardInteractor.isBouncerShowing
+ .sample(
+ combine(
+ keyguardInteractor.wakefulnessModel,
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
+ if (
+ !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
+ ) {
+ val to =
+ if (
+ wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
+ wakefulnessState.state == WakefulnessState.ASLEEP
+ ) {
+ KeyguardState.AOD
+ } else {
+ KeyguardState.LOCKSCREEN
+ }
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.BOUNCER,
+ to = to,
+ animator = getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForBouncerToGone() {
scope.launch {
keyguardInteractor.isKeyguardGoingAway
.sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 95d9602..fd2d271 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -21,36 +21,46 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@SysUISingleton
-class LockscreenGoneTransitionInteractor
+class FromDozingTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
-) : TransitionInteractor(LockscreenGoneTransitionInteractor::class.simpleName!!) {
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor(FromDozingTransitionInteractor::class.simpleName!!) {
override fun start() {
+ listenForDozingToLockscreen()
+ }
+
+ private fun listenForDozingToLockscreen() {
scope.launch {
- keyguardInteractor.isKeyguardGoingAway
+ keyguardInteractor.dozeTransitionModel
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
- val (isKeyguardGoingAway, lastStartedStep) = pair
- if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ val (dozeTransitionModel, lastStartedTransition) = pair
+ if (
+ isDozeOff(dozeTransitionModel.to) &&
+ lastStartedTransition.to == KeyguardState.DOZING
+ ) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
+ KeyguardState.DOZING,
KeyguardState.LOCKSCREEN,
- KeyguardState.GONE,
getAnimator(),
)
)
@@ -59,14 +69,14 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 10L
+ private val DEFAULT_DURATION = 500.milliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index b73ce9e..3b09ae7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -28,81 +28,48 @@
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@SysUISingleton
-class DreamingTransitionInteractor
+class FromDreamingTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(DreamingTransitionInteractor::class.simpleName!!) {
-
- private val canDreamFrom =
- setOf(KeyguardState.LOCKSCREEN, KeyguardState.GONE, KeyguardState.DOZING)
+) : TransitionInteractor(FromDreamingTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForEntryToDreaming()
listenForDreamingToLockscreen()
+ listenForDreamingToOccluded()
listenForDreamingToGone()
listenForDreamingToDozing()
}
- private fun listenForEntryToDreaming() {
- scope.launch {
- keyguardInteractor.isDreaming
- .sample(
- combine(
- keyguardInteractor.dozeTransitionModel,
- keyguardTransitionInteractor.finishedKeyguardState,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (isDreaming, dozeTransitionModel, keyguardState) = triple
- // Dozing/AOD and dreaming have overlapping events. If the state remains in
- // FINISH, it means that doze mode is not running and DREAMING is ok to
- // commence.
- if (
- isDozeOff(dozeTransitionModel.to) &&
- isDreaming &&
- canDreamFrom.contains(keyguardState)
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- name,
- keyguardState,
- KeyguardState.DREAMING,
- getAnimator(),
- )
- )
- }
- }
- }
- }
-
private fun listenForDreamingToLockscreen() {
scope.launch {
- keyguardInteractor.isDreaming
+ // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which
+ // otherwise would have gone through OCCLUDED first
+ keyguardInteractor.isDreamingWithOverlay
.sample(
combine(
keyguardInteractor.dozeTransitionModel,
keyguardTransitionInteractor.startedKeyguardTransitionStep,
- ::Pair,
+ ::Pair
),
::toTriple
)
.collect { triple ->
val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple
if (
- isDozeOff(dozeTransitionModel.to) &&
- !isDreaming &&
+ !isDreaming &&
+ isDozeOff(dozeTransitionModel.to) &&
lastStartedTransition.to == KeyguardState.DREAMING
) {
keyguardTransitionRepository.startTransition(
@@ -110,6 +77,42 @@
name,
KeyguardState.DREAMING,
KeyguardState.LOCKSCREEN,
+ getAnimator(TO_LOCKSCREEN_DURATION),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForDreamingToOccluded() {
+ scope.launch {
+ keyguardInteractor.isDreaming
+ .sample(
+ combine(
+ keyguardInteractor.isKeyguardOccluded,
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ ::Pair,
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isDreaming, isOccluded, lastStartedTransition) = triple
+ if (
+ isOccluded &&
+ !isDreaming &&
+ (lastStartedTransition.to == KeyguardState.DREAMING ||
+ lastStartedTransition.to == KeyguardState.LOCKSCREEN)
+ ) {
+ // At the moment, checking for LOCKSCREEN state above provides a corrective
+ // action. There's no great signal to determine when the dream is ending
+ // and a transition to OCCLUDED is beginning directly. For now, the solution
+ // is DREAMING->LOCKSCREEN->OCCLUDED
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ lastStartedTransition.to,
+ KeyguardState.OCCLUDED,
getAnimator(),
)
)
@@ -167,14 +170,15 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 500L
+ private val DEFAULT_DURATION = 500.milliseconds
+ val TO_LOCKSCREEN_DURATION = 1183.milliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index a50e759..553fafe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -30,19 +30,44 @@
import kotlinx.coroutines.launch
@SysUISingleton
-class GoneAodTransitionInteractor
+class FromGoneTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(GoneAodTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) {
override fun start() {
+ listenForGoneToAod()
+ listenForGoneToDreaming()
+ }
+
+ private fun listenForGoneToDreaming() {
+ scope.launch {
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
+ .collect { pair ->
+ val (isAbleToDream, keyguardState) = pair
+ if (isAbleToDream && keyguardState == KeyguardState.GONE) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.GONE,
+ KeyguardState.DREAMING,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForGoneToAod() {
scope.launch {
keyguardInteractor.wakefulnessModel
- .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
val (wakefulnessState, keyguardState) = pair
if (
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..326acc9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class FromLockscreenTransitionInteractor
+@Inject
+constructor(
+ @Application private val scope: CoroutineScope,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val shadeRepository: ShadeRepository,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) {
+
+ private var transitionId: UUID? = null
+
+ override fun start() {
+ listenForLockscreenToGone()
+ listenForLockscreenToOccluded()
+ listenForLockscreenToAod()
+ listenForLockscreenToBouncer()
+ listenForLockscreenToDreaming()
+ listenForLockscreenToBouncerDragging()
+ }
+
+ private fun listenForLockscreenToDreaming() {
+ scope.launch {
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isAbleToDream, lastStartedTransition) = pair
+ if (isAbleToDream && lastStartedTransition.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.DREAMING,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToBouncer() {
+ scope.launch {
+ keyguardInteractor.isBouncerShowing
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isBouncerShowing, lastStartedTransitionStep) = pair
+ if (
+ isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
+ ) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.BOUNCER,
+ animator = getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
+ private fun listenForLockscreenToBouncerDragging() {
+ scope.launch {
+ shadeRepository.shadeModel
+ .sample(
+ combine(
+ keyguardTransitionInteractor.finishedKeyguardState,
+ keyguardInteractor.statusBarState,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (shadeModel, keyguardState, statusBarState) = triple
+
+ val id = transitionId
+ if (id != null) {
+ // An existing `id` means a transition is started, and calls to
+ // `updateTransition` will control it until FINISHED
+ keyguardTransitionRepository.updateTransition(
+ id,
+ 1f - shadeModel.expansionAmount,
+ if (
+ shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
+ ) {
+ transitionId = null
+ TransitionState.FINISHED
+ } else {
+ TransitionState.RUNNING
+ }
+ )
+ } else {
+ // TODO (b/251849525): Remove statusbarstate check when that state is
+ // integrated into KeyguardTransitionRepository
+ if (
+ keyguardState == KeyguardState.LOCKSCREEN &&
+ shadeModel.isUserDragging &&
+ statusBarState == KEYGUARD
+ ) {
+ transitionId =
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.BOUNCER,
+ animator = null,
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToGone() {
+ scope.launch {
+ keyguardInteractor.isKeyguardGoingAway
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isKeyguardGoingAway, lastStartedStep) = pair
+ if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToOccluded() {
+ scope.launch {
+ keyguardInteractor.isKeyguardOccluded
+ .sample(
+ combine(
+ keyguardTransitionInteractor.finishedKeyguardState,
+ keyguardInteractor.isDreaming,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isOccluded, keyguardState, isDreaming) = triple
+ // Occlusion signals come from the framework, and should interrupt any
+ // existing transition
+ if (isOccluded && !isDreaming) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ keyguardState,
+ KeyguardState.OCCLUDED,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToAod() {
+ scope.launch {
+ keyguardInteractor
+ .dozeTransitionTo(DozeStateModel.DOZE_AOD)
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (dozeToAod, lastStartedStep) = pair
+ if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.AOD,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun getAnimator(): ValueAnimator {
+ return ValueAnimator().apply {
+ setInterpolator(Interpolators.LINEAR)
+ setDuration(TRANSITION_DURATION_MS)
+ }
+ }
+
+ companion object {
+ private const val TRANSITION_DURATION_MS = 500L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
similarity index 62%
copy from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index f3d2905..8878901 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -21,42 +21,43 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@SysUISingleton
-class AodLockscreenTransitionInteractor
+class FromOccludedTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodLockscreenTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromOccludedTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForTransitionToAodFromLockscreen()
- listenForTransitionToLockscreenFromDozeStates()
+ listenForOccludedToLockscreen()
+ listenForOccludedToDreaming()
}
- private fun listenForTransitionToAodFromLockscreen() {
+ private fun listenForOccludedToDreaming() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.DOZE_AOD)
- .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ val (isAbleToDream, keyguardState) = pair
+ if (isAbleToDream && keyguardState == KeyguardState.OCCLUDED) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- KeyguardState.LOCKSCREEN,
- KeyguardState.AOD,
+ KeyguardState.OCCLUDED,
+ KeyguardState.DREAMING,
getAnimator(),
)
)
@@ -65,21 +66,21 @@
}
}
- private fun listenForTransitionToLockscreenFromDozeStates() {
- val canGoToLockscreen = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+ private fun listenForOccludedToLockscreen() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.FINISH)
+ keyguardInteractor.isKeyguardOccluded
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (canGoToLockscreen.contains(lastStartedStep.to)) {
+ val (isOccluded, lastStartedKeyguardState) = pair
+ // Occlusion signals come from the framework, and should interrupt any
+ // existing transition
+ if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- lastStartedStep.to,
+ KeyguardState.OCCLUDED,
KeyguardState.LOCKSCREEN,
- getAnimator(),
+ getAnimator(TO_LOCKSCREEN_DURATION),
)
)
}
@@ -87,14 +88,15 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 500L
+ private val DEFAULT_DURATION = 500.milliseconds
+ val TO_LOCKSCREEN_DURATION = 933.milliseconds
}
}
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 6912e1d..402c179 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
@@ -22,12 +22,16 @@
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.merge
/**
* Encapsulates business-logic related to the keyguard but not to a more specific part within it.
@@ -52,8 +56,27 @@
* but not vice-versa.
*/
val isDreaming: Flow<Boolean> = repository.isDreaming
+ /** Whether the system is dreaming with an overlay active */
+ val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
+
+ /**
+ * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
+ * that doze mode is not running and DREAMING is ok to commence.
+ */
+ val isAbleToDream: Flow<Boolean> =
+ merge(isDreaming, isDreamingWithOverlay)
+ .sample(
+ dozeTransitionModel,
+ { isDreaming, dozeTransitionModel ->
+ isDreaming && isDozeOff(dozeTransitionModel.to)
+ }
+ )
+ .distinctUntilChanged()
+
/** Whether the keyguard is showing or not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+ /** Whether the keyguard is occluded (covered by an activity). */
+ val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded
/** Whether the keyguard is going away. */
val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
/** Whether the bouncer is showing or not. */
@@ -74,8 +97,8 @@
/** The approximate location on the screen of the face unlock sensor, if one is available. */
val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation
- fun dozeTransitionTo(state: DozeStateModel): Flow<DozeTransitionModel> {
- return dozeTransitionModel.filter { it.to == state }
+ fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> {
+ return dozeTransitionModel.filter { states.contains(it.to) }
}
fun isKeyguardShowing(): Boolean {
return repository.isKeyguardShowing()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index bb8b79a..fbed446 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -37,13 +37,13 @@
// exhaustive
val ret =
when (it) {
- is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
- is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
- is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it")
- is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is BouncerToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is DreamingTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromAodTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromDreamingTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromOccludedTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromDozingTransitionInteractor -> Log.d(TAG, "Started $it")
}
it.start()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 54a4f49..04024be 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -19,12 +19,17 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
+import kotlin.time.Duration
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
@@ -43,6 +48,14 @@
/** LOCKSCREEN->AOD transition information. */
val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+ /** DREAMING->LOCKSCREEN transition information. */
+ val dreamingToLockscreenTransition: Flow<TransitionStep> =
+ repository.transition(DREAMING, LOCKSCREEN)
+
+ /** OCCLUDED->LOCKSCREEN transition information. */
+ val occludedToLockscreenTransition: Flow<TransitionStep> =
+ repository.transition(OCCLUDED, LOCKSCREEN)
+
/** (any)->AOD transition information */
val anyStateToAodTransition: Flow<TransitionStep> =
repository.transitions.filter { step -> step.to == KeyguardState.AOD }
@@ -72,4 +85,28 @@
/* The last completed [KeyguardState] transition */
val finishedKeyguardState: Flow<KeyguardState> =
finishedKeyguardTransitionStep.map { step -> step.to }
+
+ /**
+ * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the
+ * range of [0, 1]. View animations should begin and end within a subset of this range. This
+ * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
+ */
+ fun transitionStepAnimation(
+ flow: Flow<TransitionStep>,
+ params: AnimationParams,
+ totalDuration: Duration,
+ ): Flow<Float> {
+ val start = (params.startTime / totalDuration).toFloat()
+ val chunks = (totalDuration / params.duration).toFloat()
+ return flow
+ // When starting, emit a value of 0f to give animations a chance to set initial state
+ .map { step ->
+ if (step.transitionState == STARTED) {
+ 0f
+ } else {
+ (step.value - start) * chunks
+ }
+ }
+ .filter { value -> value >= 0f && value <= 1f }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 6e25200..a59c407 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -89,6 +89,7 @@
KeyguardState.BOUNCER -> true
KeyguardState.LOCKSCREEN -> true
KeyguardState.GONE -> true
+ KeyguardState.OCCLUDED -> true
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
deleted file mode 100644
index 5cb7d70..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.keyguard.domain.interactor
-
-import android.animation.ValueAnimator
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
-import com.android.systemui.keyguard.shared.model.TransitionInfo
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.WakefulnessState
-import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.util.kotlin.sample
-import java.util.UUID
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class LockscreenBouncerTransitionInteractor
-@Inject
-constructor(
- @Application private val scope: CoroutineScope,
- private val keyguardInteractor: KeyguardInteractor,
- private val shadeRepository: ShadeRepository,
- private val keyguardTransitionRepository: KeyguardTransitionRepository,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor
-) : TransitionInteractor(LockscreenBouncerTransitionInteractor::class.simpleName!!) {
-
- private var transitionId: UUID? = null
-
- override fun start() {
- listenForDraggingUpToBouncer()
- listenForBouncer()
- }
-
- private fun listenForBouncer() {
- scope.launch {
- keyguardInteractor.isBouncerShowing
- .sample(
- combine(
- keyguardInteractor.wakefulnessModel,
- keyguardTransitionInteractor.startedKeyguardTransitionStep,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
- if (
- !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
- ) {
- val to =
- if (
- wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
- wakefulnessState.state == WakefulnessState.ASLEEP
- ) {
- KeyguardState.AOD
- } else {
- KeyguardState.LOCKSCREEN
- }
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.BOUNCER,
- to = to,
- animator = getAnimator(),
- )
- )
- } else if (
- isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.BOUNCER,
- animator = getAnimator(),
- )
- )
- }
- Unit
- }
- }
- }
-
- /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
- private fun listenForDraggingUpToBouncer() {
- scope.launch {
- shadeRepository.shadeModel
- .sample(
- combine(
- keyguardTransitionInteractor.finishedKeyguardState,
- keyguardInteractor.statusBarState,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (shadeModel, keyguardState, statusBarState) = triple
-
- val id = transitionId
- if (id != null) {
- // An existing `id` means a transition is started, and calls to
- // `updateTransition` will control it until FINISHED
- keyguardTransitionRepository.updateTransition(
- id,
- 1f - shadeModel.expansionAmount,
- if (
- shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
- ) {
- transitionId = null
- TransitionState.FINISHED
- } else {
- TransitionState.RUNNING
- }
- )
- } else {
- // TODO (b/251849525): Remove statusbarstate check when that state is
- // integrated into KeyguardTransitionRepository
- if (
- keyguardState == KeyguardState.LOCKSCREEN &&
- shadeModel.isUserDragging &&
- statusBarState == KEYGUARD
- ) {
- transitionId =
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.BOUNCER,
- animator = null,
- )
- )
- }
- }
- }
- }
- }
-
- private fun getAnimator(): ValueAnimator {
- return ValueAnimator().apply {
- setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
- }
- }
-
- companion object {
- private const val TRANSITION_DURATION_MS = 300L
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
index 5f63ae7..81fa233 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -32,25 +32,25 @@
@Binds
@IntoSet
- abstract fun lockscreenBouncer(
- impl: LockscreenBouncerTransitionInteractor
- ): TransitionInteractor
+ abstract fun fromBouncer(impl: FromBouncerTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor
+ abstract fun fromLockscreen(impl: FromLockscreenTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor
+ @Binds @IntoSet abstract fun fromAod(impl: FromAodTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun aodGone(impl: AodToGoneTransitionInteractor): TransitionInteractor
+ @Binds @IntoSet abstract fun fromGone(impl: FromGoneTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun bouncerGone(impl: BouncerToGoneTransitionInteractor): TransitionInteractor
+ abstract fun fromDreaming(impl: FromDreamingTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor
+ abstract fun fromOccluded(impl: FromOccludedTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun dreaming(impl: DreamingTransitionInteractor): TransitionInteractor
+ @Binds
+ @IntoSet
+ abstract fun fromDozing(impl: FromDozingTransitionInteractor): TransitionInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 08ad3d5..4d24c14 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -31,4 +31,6 @@
abstract fun start()
fun <A, B, C> toTriple(a: A, bc: Pair<B, C>) = Triple(a, bc.first, bc.second)
+
+ fun <A, B, C> toTriple(ab: Pair<A, B>, c: C) = Triple(ab.first, ab.second, c)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
new file mode 100644
index 0000000..67733e9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+
+/** Animation parameters */
+data class AnimationParams(
+ val startTime: Duration = 0.milliseconds,
+ val duration: Duration,
+)
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 dd908c4..c757986 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
@@ -57,4 +57,8 @@
* with SWIPE security method or face unlock without bypass.
*/
GONE,
+ /*
+ * An activity is displaying over the keyguard.
+ */
+ OCCLUDED,
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index ae8edfe..b19795c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -18,6 +18,7 @@
import android.annotation.SuppressLint
import android.graphics.drawable.Animatable2
+import android.os.VibrationEffect
import android.util.Size
import android.util.TypedValue
import android.view.MotionEvent
@@ -43,8 +44,11 @@
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.kotlin.pairwise
import kotlin.math.pow
import kotlin.math.sqrt
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -93,6 +97,7 @@
view: ViewGroup,
viewModel: KeyguardBottomAreaViewModel,
falsingManager: FalsingManager?,
+ vibratorHelper: VibratorHelper?,
messageDisplayer: (Int) -> Unit,
): Binding {
val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
@@ -118,22 +123,48 @@
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
+ vibratorHelper = vibratorHelper,
)
}
}
launch {
+ viewModel.startButton
+ .map { it.isActivated }
+ .pairwise()
+ .collect { (prev, next) ->
+ when {
+ !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
+ prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
+ }
+ }
+ }
+
+ launch {
viewModel.endButton.collect { buttonModel ->
updateButton(
view = endButton,
viewModel = buttonModel,
falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
+ vibratorHelper = vibratorHelper,
)
}
}
launch {
+ viewModel.endButton
+ .map { it.isActivated }
+ .pairwise()
+ .collect { (prev, next) ->
+ when {
+ !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
+ prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
+ }
+ }
+ }
+
+ launch {
viewModel.isOverlayContainerVisible.collect { isVisible ->
overlayContainer.visibility =
if (isVisible) {
@@ -239,6 +270,7 @@
viewModel: KeyguardQuickAffordanceViewModel,
falsingManager: FalsingManager?,
messageDisplayer: (Int) -> Unit,
+ vibratorHelper: VibratorHelper?,
) {
if (!viewModel.isVisible) {
view.isVisible = false
@@ -312,7 +344,9 @@
view.isClickable = viewModel.isClickable
if (viewModel.isClickable) {
if (viewModel.useLongPress) {
- view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer))
+ view.setOnTouchListener(
+ OnTouchListener(view, viewModel, messageDisplayer, vibratorHelper)
+ )
} else {
view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
}
@@ -328,6 +362,7 @@
private val view: View,
private val viewModel: KeyguardQuickAffordanceViewModel,
private val messageDisplayer: (Int) -> Unit,
+ private val vibratorHelper: VibratorHelper?,
) : View.OnTouchListener {
private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
@@ -376,25 +411,38 @@
true
}
MotionEvent.ACTION_UP -> {
- if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
- messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
- val shakeAnimator =
- ObjectAnimator.ofFloat(
- view,
- "translationX",
- 0f,
- view.context.resources
- .getDimensionPixelSize(
- R.dimen.keyguard_affordance_shake_amplitude
+ cancel(
+ onAnimationEnd =
+ if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
+ Runnable {
+ messageDisplayer.invoke(
+ R.string.keyguard_affordance_press_too_short
)
- .toFloat(),
- 0f,
- )
- shakeAnimator.duration = 300
- shakeAnimator.interpolator = CycleInterpolator(5f)
- shakeAnimator.start()
- }
- cancel()
+ val amplitude =
+ view.context.resources
+ .getDimensionPixelSize(
+ R.dimen.keyguard_affordance_shake_amplitude
+ )
+ .toFloat()
+ val shakeAnimator =
+ ObjectAnimator.ofFloat(
+ view,
+ "translationX",
+ -amplitude / 2,
+ amplitude / 2,
+ )
+ shakeAnimator.duration =
+ ShakeAnimationDuration.inWholeMilliseconds
+ shakeAnimator.interpolator =
+ CycleInterpolator(ShakeAnimationCycles)
+ shakeAnimator.start()
+
+ vibratorHelper?.vibrate(Vibrations.Shake)
+ }
+ } else {
+ null
+ }
+ )
true
}
MotionEvent.ACTION_CANCEL -> {
@@ -405,11 +453,11 @@
}
}
- private fun cancel() {
+ private fun cancel(onAnimationEnd: Runnable? = null) {
downTimestamp = 0L
longPressAnimator?.cancel()
longPressAnimator = null
- view.animate().scaleX(1f).scaleY(1f)
+ view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
}
companion object {
@@ -461,4 +509,58 @@
val indicationTextSizePx: Int,
val buttonSizePx: Size,
)
+
+ private val ShakeAnimationDuration = 300.milliseconds
+ private val ShakeAnimationCycles = 5f
+
+ object Vibrations {
+
+ private const val SmallVibrationScale = 0.3f
+ private const val BigVibrationScale = 0.6f
+
+ val Shake =
+ VibrationEffect.startComposition()
+ .apply {
+ val vibrationDelayMs =
+ (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2))
+ .toInt()
+ val vibrationCount = ShakeAnimationCycles.toInt() * 2
+ repeat(vibrationCount) {
+ addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ SmallVibrationScale,
+ vibrationDelayMs,
+ )
+ }
+ }
+ .compose()
+
+ val Activated =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ BigVibrationScale,
+ 0,
+ )
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
+ 0.1f,
+ 0,
+ )
+ .compose()
+
+ val Deactivated =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ BigVibrationScale,
+ 0,
+ )
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
+ 0.1f,
+ 0,
+ )
+ .compose()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
index 3d5985c5..f772b17 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -105,6 +105,14 @@
}
launch {
+ viewModel.showWithFullExpansion.collect { model ->
+ hostViewController.resetSecurityContainer()
+ hostViewController.showPromptReason(model.promptReason)
+ hostViewController.onResume()
+ }
+ }
+
+ launch {
viewModel.hide.collect {
hostViewController.cancelDismissAction()
hostViewController.cleanUp()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..e164f5d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class DreamingToLockscreenTransitionViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardTransitionInteractor,
+) {
+
+ /** Dream overlay y-translation on exit */
+ fun dreamOverlayTranslationY(translatePx: Int): Flow<Float> {
+ return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value ->
+ EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx
+ }
+ }
+ /** Dream overlay views alpha - fade out */
+ val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
+
+ /** Lockscreen views y-translation */
+ fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+ return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+ -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+ }
+ }
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
+
+ private fun flowForAnimation(params: AnimationParams): Flow<Float> {
+ return interactor.transitionStepAnimation(
+ interactor.dreamingToLockscreenTransition,
+ params,
+ totalDuration = TO_LOCKSCREEN_DURATION
+ )
+ }
+
+ companion object {
+ /* Length of time before ending the dream activity, in order to start unoccluding */
+ val DREAM_ANIMATION_DURATION = 250.milliseconds
+ @JvmField
+ val LOCKSCREEN_ANIMATION_DURATION_MS =
+ (TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
+
+ val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
+ val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
+ val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
+ val LOCKSCREEN_ALPHA =
+ AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
index 737c35d..e5d4e49 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -22,8 +22,10 @@
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
/** Models UI state for the lock screen bouncer; handles user input. */
@@ -42,6 +44,10 @@
/** Observe whether bouncer is showing. */
val show: Flow<KeyguardBouncerModel> = interactor.show
+ /** Observe visible expansion when bouncer is showing. */
+ val showWithFullExpansion: Flow<KeyguardBouncerModel> =
+ interactor.show.filter { it.expansionAmount == EXPANSION_VISIBLE }
+
/** Observe whether bouncer is hiding. */
val hide: Flow<Unit> = interactor.hide
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..e804562
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class OccludedToLockscreenTransitionViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardTransitionInteractor,
+) {
+ /** Lockscreen views y-translation */
+ fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+ return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+ -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+ }
+ }
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
+
+ private fun flowForAnimation(params: AnimationParams): Flow<Float> {
+ return interactor.transitionStepAnimation(
+ interactor.occludedToLockscreenTransition,
+ params,
+ totalDuration = TO_LOCKSCREEN_DURATION
+ )
+ }
+
+ companion object {
+ @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
+ val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
+ val LOCKSCREEN_ALPHA =
+ AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
index ab93b29..d6f941d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
@@ -58,10 +58,10 @@
// Keep track of the key used for the session tokens. This information is used to know when to
// dispatch a removed event so that a media object for a local session will be removed.
- private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf()
+ private val keyedTokens: MutableMap<String, MutableSet<TokenId>> = mutableMapOf()
// Keep track of which media session tokens have associated notifications.
- private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
+ private val tokensWithNotifications: MutableSet<TokenId> = mutableSetOf()
private val sessionListener =
object : MediaSessionManager.OnActiveSessionsChangedListener {
@@ -101,15 +101,15 @@
isSsReactivated: Boolean
) {
backgroundExecutor.execute {
- data.token?.let { tokensWithNotifications.add(it) }
+ data.token?.let { tokensWithNotifications.add(TokenId(it)) }
val isMigration = oldKey != null && key != oldKey
if (isMigration) {
keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
}
if (data.token != null) {
- keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) }
+ keyedTokens.get(key)?.let { tokens -> tokens.add(TokenId(data.token)) }
?: run {
- val tokens = mutableSetOf(data.token)
+ val tokens = mutableSetOf(TokenId(data.token))
keyedTokens.put(key, tokens)
}
}
@@ -125,7 +125,7 @@
isMigration ||
remote == null ||
remote.sessionToken == data.token ||
- !tokensWithNotifications.contains(remote.sessionToken)
+ !tokensWithNotifications.contains(TokenId(remote.sessionToken))
) {
// Not filtering in this case. Passing the event along to listeners.
dispatchMediaDataLoaded(key, oldKey, data, immediately)
@@ -136,7 +136,7 @@
// If the local session uses a different notification key, then lets go a step
// farther and dismiss the media data so that media controls for the local session
// don't hang around while casting.
- if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) {
+ if (!keyedTokens.get(key)!!.contains(TokenId(remote.sessionToken))) {
dispatchMediaDataRemoved(key)
}
}
@@ -199,6 +199,15 @@
packageControllers.put(controller.packageName, tokens)
}
}
- tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
+ tokensWithNotifications.retainAll(controllers.map { TokenId(it.sessionToken) })
+ }
+
+ /**
+ * Represents a unique identifier for a [MediaSession.Token].
+ *
+ * It's used to avoid storing strong binders for media session tokens.
+ */
+ private data class TokenId(val id: Int) {
+ constructor(token: MediaSession.Token) : this(token.hashCode())
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
index 03bc935..8a565fa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
@@ -26,4 +26,8 @@
class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags) {
/** */
fun isMediaTttEnabled(): Boolean = featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER)
+
+ /** Check whether the flag for the receiver success state is enabled. */
+ fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
+ featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 7b9d0b4..889147b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -37,6 +37,7 @@
import com.android.systemui.common.ui.binder.TintedIconViewBinder
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.media.taptotransfer.common.MediaTttIcon
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
@@ -69,6 +70,7 @@
mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
+ dumpManager: DumpManager,
powerManager: PowerManager,
@Main private val mainHandler: Handler,
private val mediaTttFlags: MediaTttFlags,
@@ -83,6 +85,7 @@
mainExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
R.layout.media_ttt_chip_receiver,
wakeLockBuilder,
@@ -111,6 +114,9 @@
}
}
+ private var maxRippleWidth: Float = 0f
+ private var maxRippleHeight: Float = 0f
+
private fun updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState displayState: Int,
routeInfo: MediaRoute2Info,
@@ -162,6 +168,7 @@
}
override fun start() {
+ super.start()
if (mediaTttFlags.isMediaTttEnabled()) {
commandQueue.addCallback(commandQueueCallbacks)
}
@@ -212,7 +219,7 @@
expandRipple(view.requireViewById(R.id.ripple))
}
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val appIconView = view.getAppIconView()
appIconView.animate()
.translationYBy(getTranslationAmount().toFloat())
@@ -222,7 +229,14 @@
.alpha(0f)
.setDuration(ICON_ALPHA_ANIM_DURATION)
.start()
- (view.requireViewById(R.id.ripple) as ReceiverChipRippleView).collapseRipple(onAnimationEnd)
+
+ val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
+ if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
+ mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
+ expandRippleToFull(rippleView, onAnimationEnd)
+ } else {
+ rippleView.collapseRipple(onAnimationEnd)
+ }
}
override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -267,12 +281,19 @@
})
}
- private fun layoutRipple(rippleView: ReceiverChipRippleView) {
+ private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
val windowBounds = windowManager.currentWindowMetrics.bounds
val height = windowBounds.height().toFloat()
val width = windowBounds.width().toFloat()
- rippleView.setMaxSize(width / 2f, height / 2f)
+ if (isFullScreen) {
+ maxRippleHeight = height * 2f
+ maxRippleWidth = width * 2f
+ } else {
+ maxRippleHeight = height / 2f
+ maxRippleWidth = width / 2f
+ }
+ rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
// Center the ripple on the bottom of the screen in the middle.
rippleView.setCenter(width * 0.5f, height)
val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
@@ -282,6 +303,11 @@
private fun View.getAppIconView(): CachingIconView {
return this.requireViewById(R.id.app_icon)
}
+
+ private fun expandRippleToFull(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
+ layoutRipple(rippleView, true)
+ rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+ }
}
val ICON_TRANSLATION_ANIM_DURATION = 30.frames
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 6e9fc5c..87b2528 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -22,6 +22,7 @@
import android.util.AttributeSet
import com.android.systemui.surfaceeffects.ripple.RippleShader
import com.android.systemui.surfaceeffects.ripple.RippleView
+import kotlin.math.pow
/**
* An expanding ripple effect for the media tap-to-transfer receiver chip.
@@ -59,4 +60,44 @@
})
animator.reverse()
}
+
+ // Expands the ripple to cover full screen.
+ fun expandToFull(newHeight: Float, onAnimationEnd: Runnable? = null) {
+ if (!isStarted) {
+ return
+ }
+ // Reset all listeners to animator.
+ animator.removeAllListeners()
+ animator.removeAllUpdateListeners()
+
+ // Only show the outline as ripple expands and disappears when animation ends.
+ setRippleFill(false)
+
+ val startingPercentage = calculateStartingPercentage(newHeight)
+ animator.addUpdateListener { updateListener ->
+ val now = updateListener.currentPlayTime
+ val progress = updateListener.animatedValue as Float
+ rippleShader.progress = startingPercentage + (progress * (1 - startingPercentage))
+ rippleShader.distortionStrength = 1 - rippleShader.progress
+ rippleShader.pixelDensity = 1 - rippleShader.progress
+ rippleShader.time = now.toFloat()
+ invalidate()
+ }
+ animator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ animation?.let { visibility = GONE }
+ onAnimationEnd?.run()
+ isStarted = false
+ }
+ })
+ animator.start()
+ }
+
+ // Calculates the actual starting percentage according to ripple shader progress set method.
+ // Check calculations in [RippleShader.progress]
+ fun calculateStartingPercentage(newHeight: Float): Float {
+ val ratio = rippleShader.currentHeight / newHeight
+ val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
+ return 1 - remainingPercentage
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index 7cc95a1..fba5f63 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -27,11 +27,15 @@
import androidx.activity.ComponentActivity;
import androidx.lifecycle.ViewModelProvider;
+import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.people.ui.view.PeopleViewBinder;
import com.android.systemui.people.ui.viewmodel.PeopleViewModel;
import javax.inject.Inject;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
+
/** People Tile Widget configuration activity that shows the user their conversation tiles. */
public class PeopleSpaceActivity extends ComponentActivity {
@@ -58,13 +62,18 @@
int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
viewModel.onWidgetIdChanged(widgetId);
- ViewGroup view = PeopleViewBinder.create(this);
- PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this,
- (result) -> {
- finishActivity(result);
- return null;
- });
- setContentView(view);
+ Function1<PeopleViewModel.Result, Unit> onResult = (result) -> {
+ finishActivity(result);
+ return null;
+ };
+
+ if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+ ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult);
+ } else {
+ ViewGroup view = PeopleViewBinder.create(this);
+ PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult);
+ setContentView(view);
+ }
}
private void finishActivity(PeopleViewModel.Result result) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index a6447a5..5716a1d72 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -448,6 +448,10 @@
// Any cleanup needed when the service is being destroyed.
void onDestroy() {
+ if (mSaveInBgTask != null) {
+ // just log success/failure for the pre-existing screenshot
+ mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
+ }
removeWindow();
releaseMediaPlayer();
releaseContext();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
index ba779c6..639172f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
@@ -19,6 +19,9 @@
import android.content.Context
import android.view.ViewGroup
import com.android.systemui.R
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START
@@ -30,17 +33,24 @@
@SysUIUnfoldScope
class NotificationPanelUnfoldAnimationController
@Inject
-constructor(private val context: Context, progressProvider: NaturalRotationUnfoldProgressProvider) {
+constructor(
+ private val context: Context,
+ statusBarStateController: StatusBarStateController,
+ progressProvider: NaturalRotationUnfoldProgressProvider,
+) {
+
+ private val filterShade: () -> Boolean = { statusBarStateController.getState() == SHADE ||
+ statusBarStateController.getState() == SHADE_LOCKED }
private val translateAnimator by lazy {
UnfoldConstantTranslateAnimator(
viewsIdToTranslate =
setOf(
- ViewIdToTranslate(R.id.quick_settings_panel, START),
- ViewIdToTranslate(R.id.notification_stack_scroller, END),
- ViewIdToTranslate(R.id.rightLayout, END),
- ViewIdToTranslate(R.id.clock, START),
- ViewIdToTranslate(R.id.date, START)),
+ ViewIdToTranslate(R.id.quick_settings_panel, START, filterShade),
+ ViewIdToTranslate(R.id.notification_stack_scroller, END, filterShade),
+ ViewIdToTranslate(R.id.rightLayout, END, filterShade),
+ ViewIdToTranslate(R.id.clock, START, filterShade),
+ ViewIdToTranslate(R.id.date, START, filterShade)),
progressProvider = progressProvider)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index fcdde79..8f512d0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -44,6 +44,7 @@
import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
import static com.android.systemui.util.DumpUtilsKt.asIndenting;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import static java.lang.Float.isNaN;
@@ -138,7 +139,12 @@
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
@@ -233,6 +239,8 @@
import javax.inject.Inject;
import javax.inject.Provider;
+import kotlinx.coroutines.CoroutineDispatcher;
+
@CentralSurfacesComponent.CentralSurfacesScope
public final class NotificationPanelViewController implements Dumpable {
@@ -677,6 +685,15 @@
private boolean mGestureWaitForTouchSlop;
private boolean mIgnoreXTouchSlop;
private boolean mExpandLatencyTracking;
+ private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+ private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+
+ private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ private CoroutineDispatcher mMainDispatcher;
+ private boolean mIsToLockscreenTransitionRunning = false;
+ private int mDreamingToLockscreenTransitionTranslationY;
+ private int mOccludedToLockscreenTransitionTranslationY;
+ private boolean mUnocclusionTransitionFlagEnabled = false;
private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@@ -692,6 +709,18 @@
}
};
+ private final Consumer<TransitionStep> mDreamingToLockscreenTransition =
+ (TransitionStep step) -> {
+ mIsToLockscreenTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
+ private final Consumer<TransitionStep> mOccludedToLockscreenTransition =
+ (TransitionStep step) -> {
+ mIsToLockscreenTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
@Inject
public NotificationPanelViewController(NotificationPanelView view,
@Main Handler handler,
@@ -760,6 +789,10 @@
SystemClock systemClock,
KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
+ DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
+ OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel,
+ @Main CoroutineDispatcher mainDispatcher,
+ KeyguardTransitionInteractor keyguardTransitionInteractor,
DumpManager dumpManager) {
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
@@ -775,6 +808,9 @@
mShadeLog = shadeLogger;
mShadeHeightLogger = shadeHeightLogger;
mGutsManager = gutsManager;
+ mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
+ mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel;
+ mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
@@ -852,6 +888,7 @@
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
+ mMainDispatcher = mainDispatcher;
mAccessibilityManager = accessibilityManager;
mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
setPanelAlpha(255, false /* animate */);
@@ -920,6 +957,8 @@
mNotificationPanelUnfoldAnimationController = unfoldComponent.map(
SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
+ mUnocclusionTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
+
mQsFrameTranslateController = qsFrameTranslateController;
updateUserSwitcherFlags();
mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
@@ -1072,6 +1111,30 @@
mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
controller.setup(mNotificationContainerParent));
+
+ if (mUnocclusionTransitionFlagEnabled) {
+ // Dreaming->Lockscreen
+ collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
+ mDreamingToLockscreenTransition, mMainDispatcher);
+ collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
+ toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(
+ mDreamingToLockscreenTransitionTranslationY),
+ toLockscreenTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+
+ // Occluded->Lockscreen
+ collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(),
+ mOccludedToLockscreenTransition, mMainDispatcher);
+ collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(),
+ toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY(
+ mOccludedToLockscreenTransitionTranslationY),
+ toLockscreenTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ }
}
@VisibleForTesting
@@ -1106,6 +1169,10 @@
mUdfpsMaxYBurnInOffset = mResources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
mSplitShadeScrimTransitionDistance = mResources.getDimensionPixelSize(
R.dimen.split_shade_scrim_transition_distance);
+ mDreamingToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y);
+ mOccludedToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y);
}
private void updateViewControllers(KeyguardStatusView keyguardStatusView,
@@ -1333,8 +1400,8 @@
mFalsingManager,
mLockIconViewController,
stringResourceId ->
- mKeyguardIndicationController.showTransientIndication(stringResourceId)
- );
+ mKeyguardIndicationController.showTransientIndication(stringResourceId),
+ mVibratorHelper);
}
@VisibleForTesting
@@ -1769,10 +1836,14 @@
}
private void updateClock() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
mKeyguardStatusViewController.setAlpha(alpha);
mKeyguardStatusViewController
- .setTranslationYExcludingMedia(mKeyguardOnlyTransitionTranslationY);
+ .setTranslationY(mKeyguardOnlyTransitionTranslationY, /* excludeMedia= */true);
+
if (mKeyguardQsUserSwitchController != null) {
mKeyguardQsUserSwitchController.setAlpha(alpha);
}
@@ -2656,7 +2727,9 @@
} else if (statusBarState == KEYGUARD
|| statusBarState == StatusBarState.SHADE_LOCKED) {
mKeyguardBottomArea.setVisibility(View.VISIBLE);
- mKeyguardBottomArea.setAlpha(1f);
+ if (!mIsToLockscreenTransitionRunning) {
+ mKeyguardBottomArea.setAlpha(1f);
+ }
} else {
mKeyguardBottomArea.setVisibility(View.GONE);
}
@@ -3523,6 +3596,9 @@
}
private void updateNotificationTranslucency() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
float alpha = 1f;
if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
&& !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -3578,6 +3654,9 @@
}
private void updateKeyguardBottomAreaAlpha() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
// There are two possible panel expansion behaviors:
// • User dragging up to unlock: we want to fade out as quick as possible
// (ALPHA_EXPANSION_THRESHOLD) to avoid seeing the bouncer over the bottom area.
@@ -3610,7 +3689,9 @@
}
private void onExpandingFinished() {
- mScrimController.onExpandingFinished();
+ if (!mUnocclusionTransitionFlagEnabled) {
+ mScrimController.onExpandingFinished();
+ }
mNotificationStackScrollLayoutController.onExpansionStopped();
mHeadsUpManager.onExpandingFinished();
mConversationNotificationManager.onNotificationPanelExpandStateChanged(isFullyCollapsed());
@@ -5805,6 +5886,32 @@
mCurrentPanelState = state;
}
+ private Consumer<Float> toLockscreenTransitionAlpha(
+ NotificationStackScrollLayoutController stackScroller) {
+ return (Float alpha) -> {
+ mKeyguardStatusViewController.setAlpha(alpha);
+ stackScroller.setAlpha(alpha);
+
+ mKeyguardBottomAreaInteractor.setAlpha(alpha);
+ mLockIconViewController.setAlpha(alpha);
+
+ if (mKeyguardQsUserSwitchController != null) {
+ mKeyguardQsUserSwitchController.setAlpha(alpha);
+ }
+ if (mKeyguardUserSwitcherController != null) {
+ mKeyguardUserSwitcherController.setAlpha(alpha);
+ }
+ };
+ }
+
+ private Consumer<Float> toLockscreenTransitionY(
+ NotificationStackScrollLayoutController stackScroller) {
+ return (Float translationY) -> {
+ mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false);
+ stackScroller.setTranslationY(translationY);
+ };
+ }
+
@VisibleForTesting
StatusBarStateController getStatusBarStateController() {
return mStatusBarStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index d773c01..5c1ddd6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -19,6 +19,7 @@
import android.app.StatusBarManager;
import android.media.AudioManager;
import android.media.session.MediaSessionLegacyHelper;
+import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
import android.view.GestureDetector;
@@ -238,7 +239,9 @@
() -> mService.wakeUpIfDozing(
SystemClock.uptimeMillis(),
mView,
- "LOCK_ICON_TOUCH"));
+ "LOCK_ICON_TOUCH",
+ PowerManager.WAKE_REASON_GESTURE)
+ );
// In case we start outside of the view bounds (below the status bar), we need to
// dispatch
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index bf622c9..db70065 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade
import android.hardware.display.AmbientDisplayConfiguration
+import android.os.PowerManager
import android.os.SystemClock
import android.os.UserHandle
import android.provider.Settings
@@ -89,7 +90,8 @@
centralSurfaces.wakeUpIfDozing(
SystemClock.uptimeMillis(),
notificationShadeWindowView,
- "PULSING_SINGLE_TAP"
+ "PULSING_SINGLE_TAP",
+ PowerManager.WAKE_REASON_TAP
)
}
return true
@@ -114,7 +116,9 @@
centralSurfaces.wakeUpIfDozing(
SystemClock.uptimeMillis(),
notificationShadeWindowView,
- "PULSING_DOUBLE_TAP")
+ "PULSING_DOUBLE_TAP",
+ PowerManager.WAKE_REASON_TAP
+ )
return true
}
return false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 770a236..b7001e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -930,8 +930,7 @@
if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) {
return; // udfps affordance is highlighted, no need to show action to unlock
- } else if (!mKeyguardUpdateMonitor.getIsFaceAuthenticated()
- && mKeyguardUpdateMonitor.isFaceEnrolled()) {
+ } else if (mKeyguardUpdateMonitor.isFaceEnrolled()) {
String message = mContext.getString(R.string.keyguard_retry);
mStatusBarKeyguardViewManager.setKeyguardMessage(message, mInitialTextColorState);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 2334a4c..9421524 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -90,8 +90,13 @@
class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
// Interpolator that reveals >80% of the content at 0.5 progress, makes revealing faster
- private val interpolator = PathInterpolator(/* controlX1= */ 0.4f, /* controlY1= */ 0f,
- /* controlX2= */ 0.2f, /* controlY2= */ 1f)
+ private val interpolator =
+ PathInterpolator(
+ /* controlX1= */ 0.4f,
+ /* controlY1= */ 0f,
+ /* controlX2= */ 0.2f,
+ /* controlY2= */ 1f
+ )
override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
val interpolatedAmount = interpolator.getInterpolation(amount)
@@ -116,17 +121,17 @@
if (isVertical) {
scrim.setRevealGradientBounds(
- left = scrim.width / 2 - (scrim.width / 2) * gradientBoundsAmount,
+ left = scrim.viewWidth / 2 - (scrim.viewWidth / 2) * gradientBoundsAmount,
top = 0f,
- right = scrim.width / 2 + (scrim.width / 2) * gradientBoundsAmount,
- bottom = scrim.height.toFloat()
+ right = scrim.viewWidth / 2 + (scrim.viewWidth / 2) * gradientBoundsAmount,
+ bottom = scrim.viewHeight.toFloat()
)
} else {
scrim.setRevealGradientBounds(
left = 0f,
- top = scrim.height / 2 - (scrim.height / 2) * gradientBoundsAmount,
- right = scrim.width.toFloat(),
- bottom = scrim.height / 2 + (scrim.height / 2) * gradientBoundsAmount
+ top = scrim.viewHeight / 2 - (scrim.viewHeight / 2) * gradientBoundsAmount,
+ right = scrim.viewWidth.toFloat(),
+ bottom = scrim.viewHeight / 2 + (scrim.viewHeight / 2) * gradientBoundsAmount
)
}
}
@@ -234,7 +239,14 @@
* transparent center. The center position, size, and stops of the gradient can be manipulated to
* reveal views below the scrim as if they are being 'lit up'.
*/
-class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+class LightRevealScrim
+@JvmOverloads
+constructor(
+ context: Context?,
+ attrs: AttributeSet?,
+ initialWidth: Int? = null,
+ initialHeight: Int? = null
+) : View(context, attrs) {
/** Listener that is called if the scrim's opaqueness changes */
lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
@@ -278,6 +290,17 @@
var revealGradientHeight: Float = 0f
/**
+ * Keeps the initial value until the view is measured. See [LightRevealScrim.onMeasure].
+ *
+ * Needed as the view dimensions are used before the onMeasure pass happens, and without preset
+ * width and height some flicker during fold/unfold happens.
+ */
+ internal var viewWidth: Int = initialWidth ?: 0
+ private set
+ internal var viewHeight: Int = initialHeight ?: 0
+ private set
+
+ /**
* Alpha of the fill that can be used in the beginning of the animation to hide the content.
* Normally the gradient bounds are animated from small size so the content is not visible, but
* if the start gradient bounds allow to see some content this could be used to make the reveal
@@ -375,6 +398,11 @@
invalidate()
}
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ viewWidth = measuredWidth
+ viewHeight = measuredHeight
+ }
/**
* Sets bounds for the transparent oval gradient that reveals the views below the scrim. This is
* simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index b8302d7..905cc3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -5,6 +5,7 @@
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.Configuration
+import android.os.PowerManager
import android.os.SystemClock
import android.util.IndentingPrintWriter
import android.util.MathUtils
@@ -272,7 +273,12 @@
// Bind the click listener of the shelf to go to the full shade
notificationShelfController.setOnClickListener {
if (statusBarStateController.state == StatusBarState.KEYGUARD) {
- centralSurfaces.wakeUpIfDozing(SystemClock.uptimeMillis(), it, "SHADE_CLICK")
+ centralSurfaces.wakeUpIfDozing(
+ SystemClock.uptimeMillis(),
+ it,
+ "SHADE_CLICK",
+ PowerManager.WAKE_REASON_GESTURE,
+ )
goToLockedShade(it)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 3516037..8f9365c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.UserInfo;
+import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -64,6 +65,8 @@
import com.android.systemui.util.DumpUtilsKt;
import com.android.systemui.util.ListenerSet;
+import dagger.Lazy;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -71,8 +74,6 @@
import java.util.Optional;
import java.util.function.Consumer;
-import dagger.Lazy;
-
/**
* Class for handling remote input state over a set of notifications. This class handles things
* like keeping notifications temporarily that were cancelled as a response to a remote input
@@ -120,7 +121,8 @@
View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
mCentralSurfacesOptionalLazy.get().ifPresent(
centralSurfaces -> centralSurfaces.wakeUpIfDozing(
- SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK"));
+ SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK",
+ PowerManager.WAKE_REASON_GESTURE));
final NotificationEntry entry = getNotificationForParent(view.getParent());
mLogger.logInitialClick(entry, pendingIntent);
@@ -464,9 +466,6 @@
riv.getController().setRemoteInputs(inputs);
riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null;
- if (parent != null) {
- riv.setDefocusTargetHeight(parent.getHeight());
- }
riv.focusAnimated(parent);
if (userMessageContent != null) {
riv.setEditTextContent(userMessageContent);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
index c630feb..976924a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
@@ -22,7 +22,6 @@
import android.content.Context
import android.content.res.Configuration
import android.os.PowerManager
-import android.os.PowerManager.WAKE_REASON_GESTURE
import android.os.SystemClock
import android.util.IndentingPrintWriter
import android.view.MotionEvent
@@ -249,7 +248,7 @@
}
if (statusBarStateController.isDozing) {
wakeUpCoordinator.willWakeUp = true
- mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE,
+ mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
"com.android.systemui:PULSEDRAG")
}
lockscreenShadeTransitionController.goToLockedShade(startingChild,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 57add75..5f5418f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -1302,7 +1302,7 @@
}
}
String wifi = args.getString("wifi");
- if (wifi != null) {
+ if (wifi != null && !mStatusBarPipelineFlags.runNewWifiIconBackend()) {
boolean show = wifi.equals("show");
String level = args.getString("level");
if (level != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index c3ce593..705cf92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification;
import android.app.Notification;
+import android.os.PowerManager;
import android.os.SystemClock;
import android.service.notification.StatusBarNotification;
import android.util.Log;
@@ -70,7 +71,8 @@
}
mCentralSurfacesOptional.ifPresent(centralSurfaces -> centralSurfaces.wakeUpIfDozing(
- SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK"));
+ SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK",
+ PowerManager.WAKE_REASON_GESTURE));
final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
final NotificationEntry entry = row.getEntry();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 5db95d7..ca1e397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -146,7 +146,8 @@
private static final int DELAY_BEFORE_SHADE_CLOSE = 200;
private boolean mShadeNeedsToClose = false;
- private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
+ @VisibleForTesting
+ static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
/**
@@ -1326,8 +1327,11 @@
* @param listenerNeedsAnimation does the listener need to animate?
*/
private void updateStackPosition(boolean listenerNeedsAnimation) {
+ float topOverscrollAmount = mShouldUseSplitNotificationShade
+ ? getCurrentOverScrollAmount(true /* top */) : 0f;
final float endTopPosition = mTopPadding + mExtraTopInsetForFullShadeTransition
+ mAmbientState.getOverExpansion()
+ + topOverscrollAmount
- getCurrentOverScrollAmount(false /* top */);
float fraction = mAmbientState.getExpansionFraction();
// If we are on quick settings, we need to quickly hide it to show the bouncer to avoid an
@@ -2613,8 +2617,10 @@
float bottomAmount = getCurrentOverScrollAmount(false);
if (velocityY < 0 && topAmount > 0) {
setOwnScrollY(mOwnScrollY - (int) topAmount);
- mDontReportNextOverScroll = true;
- setOverScrollAmount(0, true, false);
+ if (!mShouldUseSplitNotificationShade) {
+ mDontReportNextOverScroll = true;
+ setOverScrollAmount(0, true, false);
+ }
mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */)
* mOverflingDistance + topAmount;
} else if (velocityY > 0 && bottomAmount > 0) {
@@ -2648,6 +2654,7 @@
float topOverScroll = getCurrentOverScrollAmount(true);
return mScrolledToTopOnFirstDown
&& !mExpandedInThisMotion
+ && !mShouldUseSplitNotificationShade
&& (initialVelocity > mMinimumVelocity
|| (topOverScroll > mMinTopOverScrollToEscape && initialVelocity > 0));
}
@@ -2713,7 +2720,7 @@
return RUBBER_BAND_FACTOR_AFTER_EXPAND;
} else if (mIsExpansionChanging || mPanelTracking) {
return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND;
- } else if (mScrolledToTopOnFirstDown) {
+ } else if (mScrolledToTopOnFirstDown && !mShouldUseSplitNotificationShade) {
return 1.0f;
}
return RUBBER_BAND_FACTOR_NORMAL;
@@ -5705,7 +5712,8 @@
}
}
- private void updateSplitNotificationShade() {
+ @VisibleForTesting
+ void updateSplitNotificationShade() {
boolean split = LargeScreenUtils.shouldUseSplitNotificationShade(getResources());
if (split != mShouldUseSplitNotificationShade) {
mShouldUseSplitNotificationShade = split;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 4bcc0b6..c2c38a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -916,6 +916,11 @@
return mView.getTranslationX();
}
+ /** Set view y-translation */
+ public void setTranslationY(float translationY) {
+ mView.setTranslationY(translationY);
+ }
+
public int indexOfChild(View view) {
return mView.indexOfChild(view);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 5e98f54..895a293 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -429,7 +429,7 @@
Runnable wakeUp = ()-> {
if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
mLogger.i("bio wakelock: Authenticated, waking up...");
- mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC,
"android.policy:BIOMETRIC");
}
Trace.beginSection("release wake-and-unlock");
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 8d06fad..4d0ad40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -25,6 +25,7 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
+import android.os.PowerManager;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.view.KeyEvent;
@@ -203,7 +204,10 @@
@Override
Lifecycle getLifecycle();
- void wakeUpIfDozing(long time, View where, String why);
+ /**
+ * Wakes up the device if the device was dozing.
+ */
+ void wakeUpIfDozing(long time, View where, String why, @PowerManager.WakeReason int wakeReason);
NotificationShadeWindowView getNotificationShadeWindowView();
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 1fcfe4e..198572a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -899,8 +899,6 @@
mKeyguardIndicationController.init();
mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener);
- mStatusBarStateController.addCallback(mStateListener,
- SysuiStatusBarStateController.RANK_STATUS_BAR);
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
@@ -1519,10 +1517,11 @@
* @param why the reason for the wake up
*/
@Override
- public void wakeUpIfDozing(long time, View where, String why) {
+ public void wakeUpIfDozing(long time, View where, String why,
+ @PowerManager.WakeReason int wakeReason) {
if (mDozing && mScreenOffAnimationController.allowWakeUpIfDozing()) {
mPowerManager.wakeUp(
- time, PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:" + why);
+ time, wakeReason, "com.android.systemui:" + why);
mWakeUpComingFromTouch = true;
mFalsingCollector.onScreenOnFromTouch();
}
@@ -1599,6 +1598,8 @@
protected void startKeyguard() {
Trace.beginSection("CentralSurfaces#startKeyguard");
+ mStatusBarStateController.addCallback(mStateListener,
+ SysuiStatusBarStateController.RANK_STATUS_BAR);
mBiometricUnlockController = mBiometricUnlockControllerLazy.get();
mBiometricUnlockController.addBiometricModeListener(
new BiometricUnlockController.BiometricModeListener() {
@@ -3379,7 +3380,8 @@
mStatusBarHideIconsForBouncerManager.setBouncerShowingAndTriggerUpdate(bouncerShowing);
mCommandQueue.recomputeDisableFlags(mDisplayId, true /* animate */);
if (mBouncerShowing) {
- wakeUpIfDozing(SystemClock.uptimeMillis(), null, "BOUNCER_VISIBLE");
+ wakeUpIfDozing(SystemClock.uptimeMillis(), null, "BOUNCER_VISIBLE",
+ PowerManager.WAKE_REASON_GESTURE);
}
updateScrimController();
if (!mBouncerShowing) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index c7be219..c72eb05 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -42,6 +42,8 @@
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -56,14 +58,17 @@
private final int mIconSize;
private StatusBarWifiView mWifiView;
+ private ModernStatusBarWifiView mModernWifiView;
private boolean mDemoMode;
private int mColor;
private final MobileIconsViewModel mMobileIconsViewModel;
+ private final StatusBarLocation mLocation;
public DemoStatusIcons(
LinearLayout statusIcons,
MobileIconsViewModel mobileIconsViewModel,
+ StatusBarLocation location,
int iconSize
) {
super(statusIcons.getContext());
@@ -71,6 +76,7 @@
mIconSize = iconSize;
mColor = DarkIconDispatcher.DEFAULT_ICON_TINT;
mMobileIconsViewModel = mobileIconsViewModel;
+ mLocation = location;
if (statusIcons instanceof StatusIconContainer) {
setShouldRestrictIcons(((StatusIconContainer) statusIcons).isRestrictingIcons());
@@ -233,14 +239,14 @@
public void addDemoWifiView(WifiIconState state) {
Log.d(TAG, "addDemoWifiView: ");
- // TODO(b/238425913): Migrate this view to {@code ModernStatusBarWifiView}.
StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot);
int viewIndex = getChildCount();
// If we have mobile views, put wifi before them
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
- if (child instanceof StatusBarMobileView) {
+ if (child instanceof StatusBarMobileView
+ || child instanceof ModernStatusBarMobileView) {
viewIndex = i;
break;
}
@@ -287,7 +293,7 @@
ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind(
mobileContext,
"mobile",
- mMobileIconsViewModel.viewModelForSub(subId)
+ mMobileIconsViewModel.viewModelForSub(subId, mLocation)
);
// mobile always goes at the end
@@ -296,6 +302,30 @@
}
/**
+ * Add a {@link ModernStatusBarWifiView}
+ */
+ public void addModernWifiView(LocationBasedWifiViewModel viewModel) {
+ Log.d(TAG, "addModernDemoWifiView: ");
+ ModernStatusBarWifiView view = ModernStatusBarWifiView
+ .constructAndBind(mContext, "wifi", viewModel);
+
+ int viewIndex = getChildCount();
+ // If we have mobile views, put wifi before them
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child instanceof StatusBarMobileView
+ || child instanceof ModernStatusBarMobileView) {
+ viewIndex = i;
+ break;
+ }
+ }
+
+ mModernWifiView = view;
+ mModernWifiView.setStaticDrawableColor(mColor);
+ addView(view, viewIndex, createLayoutParams());
+ }
+
+ /**
* Apply an update to a mobile icon view for the given {@link MobileIconState}. For
* compatibility with {@link MobileContextProvider}, we have to recreate the view every time we
* update it, since the context (and thus the {@link Configuration}) may have changed
@@ -317,8 +347,14 @@
public void onRemoveIcon(StatusIconDisplayable view) {
if (view.getSlot().equals("wifi")) {
- removeView(mWifiView);
- mWifiView = null;
+ if (view instanceof StatusBarWifiView) {
+ removeView(mWifiView);
+ mWifiView = null;
+ } else if (view instanceof ModernStatusBarWifiView) {
+ Log.d(TAG, "onRemoveIcon: removing modern wifi view");
+ removeView(mModernWifiView);
+ mModernWifiView = null;
+ }
} else if (view instanceof StatusBarMobileView) {
StatusBarMobileView mobileView = matchingMobileView(view);
if (mobileView != null) {
@@ -371,8 +407,14 @@
if (mWifiView != null) {
mWifiView.onDarkChanged(areas, darkIntensity, tint);
}
+ if (mModernWifiView != null) {
+ mModernWifiView.onDarkChanged(areas, darkIntensity, tint);
+ }
for (StatusBarMobileView view : mMobileViews) {
view.onDarkChanged(areas, darkIntensity, tint);
}
+ for (ModernStatusBarMobileView view : mModernMobileViews) {
+ view.onDarkChanged(areas, darkIntensity, tint);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
index 2ce1163..e4227dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt
@@ -30,6 +30,7 @@
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
/**
* Renders the bottom area of the lock-screen. Concerned primarily with the quick affordance UI
@@ -65,12 +66,14 @@
falsingManager: FalsingManager? = null,
lockIconViewController: LockIconViewController? = null,
messageDisplayer: MessageDisplayer? = null,
+ vibratorHelper: VibratorHelper? = null,
) {
binding =
bind(
this,
viewModel,
falsingManager,
+ vibratorHelper,
) {
messageDisplayer?.display(it)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index d500f99..ee8b861 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -232,7 +232,6 @@
private boolean mExpansionAffectsAlpha = true;
private boolean mAnimateChange;
private boolean mUpdatePending;
- private boolean mTracking;
private long mAnimationDuration = -1;
private long mAnimationDelay;
private Animator.AnimatorListener mAnimatorListener;
@@ -526,7 +525,6 @@
}
public void onTrackingStarted() {
- mTracking = true;
mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen();
if (!mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) {
mAnimatingPanelExpansionOnUnlock = false;
@@ -534,7 +532,6 @@
}
public void onExpandingFinished() {
- mTracking = false;
setUnocclusionAnimationRunning(false);
}
@@ -1450,8 +1447,6 @@
pw.print(" expansionProgress=");
pw.println(mTransitionToLockScreenFullShadeNotificationsProgress);
- pw.print(" mTracking=");
- pw.println(mTracking);
pw.print(" mDefaultScrimAlpha=");
pw.println(mDefaultScrimAlpha);
pw.print(" mPanelExpansionFraction=");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index df3ab49..1a14a036 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -359,6 +359,7 @@
// Whether or not these icons show up in dumpsys
protected boolean mShouldLog = false;
private StatusBarIconController mController;
+ private final StatusBarLocation mLocation;
// Enables SystemUI demo mode to take effect in this group
protected boolean mDemoable = true;
@@ -381,11 +382,12 @@
mContext = group.getContext();
mIconSize = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.status_bar_icon_size);
+ mLocation = location;
if (statusBarPipelineFlags.runNewMobileIconsBackend()) {
// This starts the flow for the new pipeline, and will notify us of changes if
// {@link StatusBarPipelineFlags#useNewMobileIcons} is also true.
- mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel();
+ mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
} else {
mMobileIconsViewModel = null;
@@ -394,7 +396,7 @@
if (statusBarPipelineFlags.runNewWifiIconBackend()) {
// This starts the flow for the new pipeline, and will notify us of changes if
// {@link StatusBarPipelineFlags#useNewWifiIcon} is also true.
- mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, location);
+ mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation);
} else {
mWifiViewModel = null;
}
@@ -495,6 +497,11 @@
ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot);
mGroup.addView(view, index, onCreateLayoutParams());
+
+ if (mIsInDemoMode) {
+ mDemoStatusIcons.addModernWifiView(mWifiViewModel);
+ }
+
return view;
}
@@ -569,7 +576,7 @@
.constructAndBind(
mobileContext,
slot,
- mMobileIconsViewModel.viewModelForSub(subId)
+ mMobileIconsViewModel.viewModelForSub(subId, mLocation)
);
}
@@ -686,6 +693,9 @@
mIsInDemoMode = true;
if (mDemoStatusIcons == null) {
mDemoStatusIcons = createDemoStatusIcons();
+ if (mStatusBarPipelineFlags.useNewWifiIcon()) {
+ mDemoStatusIcons.addModernWifiView(mWifiViewModel);
+ }
}
mDemoStatusIcons.onDemoModeStarted();
}
@@ -705,7 +715,12 @@
}
protected DemoStatusIcons createDemoStatusIcons() {
- return new DemoStatusIcons((LinearLayout) mGroup, mMobileIconsViewModel, mIconSize);
+ return new DemoStatusIcons(
+ (LinearLayout) mGroup,
+ mMobileIconsViewModel,
+ mLocation,
+ mIconSize
+ );
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index f196505..d480fab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -135,7 +135,7 @@
private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
private final BouncerView mPrimaryBouncerView;
- private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
+ private final Lazy<ShadeController> mShadeController;
// Local cache of expansion events, to avoid duplicates
private float mFraction = -1f;
@@ -252,6 +252,7 @@
private float mQsExpansion;
final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
private boolean mIsModernBouncerEnabled;
+ private boolean mIsUnoccludeTransitionFlagEnabled;
private OnDismissAction mAfterKeyguardGoneAction;
private Runnable mKeyguardGoneCancelAction;
@@ -329,6 +330,7 @@
mFoldAodAnimationController = sysUIUnfoldComponent
.map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
+ mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
}
@Override
@@ -867,8 +869,10 @@
// by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
reset(isOccluding /* hideBouncerWhenShowing*/);
}
- if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
- mCentralSurfaces.animateKeyguardUnoccluding();
+ if (!mIsUnoccludeTransitionFlagEnabled) {
+ if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
+ mCentralSurfaces.animateKeyguardUnoccluding();
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index a1e0c50..da1c361 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -20,6 +20,7 @@
import android.app.KeyguardManager;
import android.content.Context;
+import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -270,7 +271,8 @@
boolean nowExpanded) {
mHeadsUpManager.setExpanded(clickedEntry, nowExpanded);
mCentralSurfaces.wakeUpIfDozing(
- SystemClock.uptimeMillis(), clickedView, "NOTIFICATION_CLICK");
+ SystemClock.uptimeMillis(), clickedView, "NOTIFICATION_CLICK",
+ PowerManager.WAKE_REASON_GESTURE);
if (nowExpanded) {
if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
mShadeTransitionController.goToLockedShade(clickedEntry.getRow());
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 c350c78..0d01715 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
@@ -36,7 +36,7 @@
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.wifi.data.repository.WifiRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
import dagger.Binds
@@ -56,7 +56,7 @@
@Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
- @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
+ @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/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
index 1d00c33..1aa954f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.pipeline.mobile.data.model
import android.annotation.IntRange
-import android.telephony.Annotation.DataActivityType
import android.telephony.CellSignalStrength
import android.telephony.TelephonyCallback.CarrierNetworkListener
import android.telephony.TelephonyCallback.DataActivityListener
@@ -27,7 +26,10 @@
import android.telephony.TelephonyCallback.SignalStrengthsListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyManager
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
/**
* Data class containing all of the relevant information for a particular line of service, known as
@@ -39,30 +41,112 @@
* threading complex system objects through the pipeline.
*/
data class MobileConnectionModel(
- /** From [ServiceStateListener.onServiceStateChanged] */
+ /** Fields below are from [ServiceStateListener.onServiceStateChanged] */
val isEmergencyOnly: Boolean = false,
+ val isRoaming: Boolean = false,
+ /**
+ * See [android.telephony.ServiceState.getOperatorAlphaShort], this value is defined as the
+ * current registered operator name in short alphanumeric format. In some cases this name might
+ * be preferred over other methods of calculating the network name
+ */
+ val operatorAlphaShort: String? = null,
- /** From [SignalStrengthsListener.onSignalStrengthsChanged] */
+ /** Fields below from [SignalStrengthsListener.onSignalStrengthsChanged] */
val isGsm: Boolean = false,
@IntRange(from = 0, to = 4)
val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
@IntRange(from = 0, to = 4)
val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
- /** Mapped from [DataConnectionStateListener.onDataConnectionStateChanged] */
+ /** Fields below from [DataConnectionStateListener.onDataConnectionStateChanged] */
val dataConnectionState: DataConnectionState = Disconnected,
- /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */
- @DataActivityType val dataActivityDirection: Int? = null,
+ /**
+ * Fields below from [DataActivityListener.onDataActivity]. See [TelephonyManager] for the
+ * values
+ */
+ val dataActivityDirection: DataActivityModel =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ ),
- /** From [CarrierNetworkListener.onCarrierNetworkChange] */
+ /** Fields below from [CarrierNetworkListener.onCarrierNetworkChange] */
val carrierNetworkChangeActive: Boolean = false,
+ /** Fields below from [DisplayInfoListener.onDisplayInfoChanged]. */
+
/**
- * From [DisplayInfoListener.onDisplayInfoChanged].
- *
* [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
* [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
*/
val resolvedNetworkType: ResolvedNetworkType = ResolvedNetworkType.UnknownNetworkType,
-)
+) : Diffable<MobileConnectionModel> {
+ override fun logDiffs(prevVal: MobileConnectionModel, row: TableRowLogger) {
+ if (prevVal.dataConnectionState != dataConnectionState) {
+ row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+ }
+
+ if (prevVal.isEmergencyOnly != isEmergencyOnly) {
+ row.logChange(COL_EMERGENCY, isEmergencyOnly)
+ }
+
+ if (prevVal.isRoaming != isRoaming) {
+ row.logChange(COL_ROAMING, isRoaming)
+ }
+
+ if (prevVal.operatorAlphaShort != operatorAlphaShort) {
+ row.logChange(COL_OPERATOR, operatorAlphaShort)
+ }
+
+ if (prevVal.isGsm != isGsm) {
+ row.logChange(COL_IS_GSM, isGsm)
+ }
+
+ if (prevVal.cdmaLevel != cdmaLevel) {
+ row.logChange(COL_CDMA_LEVEL, cdmaLevel)
+ }
+
+ if (prevVal.primaryLevel != primaryLevel) {
+ row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
+ }
+
+ if (prevVal.dataActivityDirection != dataActivityDirection) {
+ row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+ }
+
+ if (prevVal.carrierNetworkChangeActive != carrierNetworkChangeActive) {
+ row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive)
+ }
+
+ if (prevVal.resolvedNetworkType != resolvedNetworkType) {
+ row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString())
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString())
+ row.logChange(COL_EMERGENCY, isEmergencyOnly)
+ row.logChange(COL_ROAMING, isRoaming)
+ row.logChange(COL_OPERATOR, operatorAlphaShort)
+ row.logChange(COL_IS_GSM, isGsm)
+ row.logChange(COL_CDMA_LEVEL, cdmaLevel)
+ row.logChange(COL_PRIMARY_LEVEL, primaryLevel)
+ row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString())
+ row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive)
+ row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString())
+ }
+
+ companion object {
+ const val COL_EMERGENCY = "EmergencyOnly"
+ const val COL_ROAMING = "Roaming"
+ const val COL_OPERATOR = "OperatorName"
+ const val COL_IS_GSM = "IsGsm"
+ const val COL_CDMA_LEVEL = "CdmaLevel"
+ const val COL_PRIMARY_LEVEL = "PrimaryLevel"
+ const val COL_CONNECTION_STATE = "ConnectionState"
+ const val COL_ACTIVITY_DIRECTION = "DataActivity"
+ const val COL_CARRIER_NETWORK_CHANGE = "CarrierNetworkChangeActive"
+ const val COL_RESOLVED_NETWORK_TYPE = "NetworkType"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
new file mode 100644
index 0000000..c50d82a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 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.mobile.data.model
+
+import android.content.Intent
+import android.telephony.TelephonyManager.EXTRA_DATA_SPN
+import android.telephony.TelephonyManager.EXTRA_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+
+/**
+ * Encapsulates the data needed to show a network name for a mobile network. The data is parsed from
+ * the intent sent by [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED].
+ */
+sealed interface NetworkNameModel : Diffable<NetworkNameModel> {
+ val name: String
+
+ /** The default name is read from [com.android.internal.R.string.lockscreen_carrier_default] */
+ data class Default(override val name: String) : NetworkNameModel {
+ override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) {
+ if (prevVal !is Default || prevVal.name != name) {
+ row.logChange(COL_NETWORK_NAME, "Default($name)")
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_NAME, "Default($name)")
+ }
+ }
+
+ /**
+ * This name has been derived from telephony intents. see
+ * [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED]
+ */
+ data class Derived(override val name: String) : NetworkNameModel {
+ override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) {
+ if (prevVal !is Derived || prevVal.name != name) {
+ row.logChange(COL_NETWORK_NAME, "Derived($name)")
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_NETWORK_NAME, "Derived($name)")
+ }
+ }
+
+ companion object {
+ const val COL_NETWORK_NAME = "networkName"
+ }
+}
+
+fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? {
+ val showSpn = getBooleanExtra(EXTRA_SHOW_SPN, false)
+ val spn = getStringExtra(EXTRA_DATA_SPN)
+ val showPlmn = getBooleanExtra(EXTRA_SHOW_PLMN, false)
+ val plmn = getStringExtra(EXTRA_PLMN)
+
+ val str = StringBuilder()
+ val strData = StringBuilder()
+ if (showPlmn && plmn != null) {
+ str.append(plmn)
+ strData.append(plmn)
+ }
+ if (showSpn && spn != null) {
+ if (str.isNotEmpty()) {
+ str.append(separator)
+ }
+ str.append(spn)
+ }
+
+ return if (str.isNotEmpty()) NetworkNameModel.Derived(str.toString()) else null
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 2621f997..40e9ba1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -20,7 +20,9 @@
import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -38,6 +40,13 @@
interface MobileConnectionRepository {
/** The subscriptionId that this connection represents */
val subId: Int
+
+ /**
+ * The table log buffer created for this connection. Will have the name "MobileConnectionLog
+ * [subId]"
+ */
+ val tableLogBuffer: TableLogBuffer
+
/**
* A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
* listener + model.
@@ -50,4 +59,15 @@
* [SubscriptionManager.getDefaultDataSubscriptionId]
*/
val isDefaultDataSubscription: StateFlow<Boolean>
+
+ /**
+ * See [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber]. This bit only matters if
+ * the connection type is CDMA.
+ *
+ * True if the Enhanced Roaming Indicator (ERI) display number is not [TelephonyManager.ERI_OFF]
+ */
+ val cdmaRoaming: StateFlow<Boolean>
+
+ /** The service provider name for this network connection, or the default name */
+ val networkName: StateFlow<NetworkNameModel>
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 1c08525..b252de8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -18,14 +18,18 @@
import android.content.Context
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import android.util.Log
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -34,6 +38,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -57,6 +62,7 @@
private val dataSource: DemoModeMobileConnectionDataSource,
@Application private val scope: CoroutineScope,
context: Context,
+ private val logFactory: TableLogBufferFactory,
) : MobileConnectionsRepository {
private var demoCommandJob: Job? = null
@@ -146,7 +152,16 @@
override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
return connectionRepoCache[subId]
- ?: DemoMobileConnectionRepository(subId).also { connectionRepoCache[subId] = it }
+ ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it }
+ }
+
+ private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository {
+ val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100)
+
+ return DemoMobileConnectionRepository(
+ subId,
+ tableLogBuffer,
+ )
}
override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
@@ -185,7 +200,9 @@
// This is always true here, because we split out disabled states at the data-source level
connection.dataEnabled.value = true
connection.isDefaultDataSubscription.value = state.dataType != null
+ connection.networkName.value = NetworkNameModel.Derived(state.name)
+ connection.cdmaRoaming.value = state.roaming
connection.connectionInfo.value = state.toMobileConnectionModel()
}
@@ -229,12 +246,13 @@
private fun Mobile.toMobileConnectionModel(): MobileConnectionModel {
return MobileConnectionModel(
isEmergencyOnly = false, // TODO(b/261029387): not yet supported
+ isRoaming = roaming,
isGsm = false, // TODO(b/261029387): not yet supported
cdmaLevel = level ?: 0,
primaryLevel = level ?: 0,
dataConnectionState =
DataConnectionState.Connected, // TODO(b/261029387): not yet supported
- dataActivityDirection = activity,
+ dataActivityDirection = (activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel(),
carrierNetworkChangeActive = carrierNetworkChange,
resolvedNetworkType = dataType.toResolvedNetworkType()
)
@@ -254,10 +272,17 @@
}
}
-class DemoMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository {
+class DemoMobileConnectionRepository(
+ override val subId: Int,
+ override val tableLogBuffer: TableLogBuffer,
+) : MobileConnectionRepository {
override val connectionInfo = MutableStateFlow(MobileConnectionModel())
override val dataEnabled = MutableStateFlow(true)
override val isDefaultDataSubscription = MutableStateFlow(true)
+
+ override val cdmaRoaming = MutableStateFlow(false)
+
+ override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo network"))
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
index da55787..d4ddb85 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
@@ -24,10 +24,8 @@
import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
@@ -35,8 +33,6 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
@@ -52,27 +48,7 @@
demoModeController: DemoModeController,
@Application scope: CoroutineScope,
) {
- private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow {
- val callback =
- object : DemoMode {
- override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK)
-
- override fun dispatchDemoCommand(command: String, args: Bundle) {
- trySend(args)
- }
-
- override fun onDemoModeFinished() {
- // Handled elsewhere
- }
-
- override fun onDemoModeStarted() {
- // Handled elsewhere
- }
- }
-
- demoModeController.addCallback(callback)
- awaitClose { demoModeController.removeCallback(callback) }
- }
+ private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
// If the args contains "mobile", then all of the args are relevant. It's just the way demo mode
// commands work and it's a little silly
@@ -98,6 +74,8 @@
val inflateStrength = getString("inflate")?.toBoolean()
val activity = getString("activity")?.toActivity()
val carrierNetworkChange = getString("carriernetworkchange") == "show"
+ val roaming = getString("roam") == "show"
+ val name = getString("networkname") ?: "demo mode"
return Mobile(
level = level,
@@ -107,6 +85,8 @@
inflateStrength = inflateStrength,
activity = activity,
carrierNetworkChange = carrierNetworkChange,
+ roaming = roaming,
+ name = name,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
index 3f3acaf..8b03f71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
@@ -34,6 +34,8 @@
val inflateStrength: Boolean?,
@DataActivityType val activity: Int?,
val carrierNetworkChange: Boolean,
+ val roaming: Boolean,
+ val name: String,
) : FakeNetworkEventModel
data class MobileDisabled(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 15505fd..0b9e158 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -17,29 +17,39 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.content.Context
+import android.content.IntentFilter
import android.database.ContentObserver
import android.provider.Settings.Global
import android.telephony.CellSignalStrength
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
import android.telephony.SignalStrength
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyCallback
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.ERI_OFF
+import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -51,6 +61,7 @@
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
@@ -61,13 +72,17 @@
class MobileConnectionRepositoryImpl(
private val context: Context,
override val subId: Int,
+ defaultNetworkName: NetworkNameModel,
+ networkNameSeparator: String,
private val telephonyManager: TelephonyManager,
private val globalSettings: GlobalSettings,
+ broadcastDispatcher: BroadcastDispatcher,
defaultDataSubId: StateFlow<Int>,
globalMobileDataSettingChangedEvent: Flow<Unit>,
mobileMappingsProxy: MobileMappingsProxy,
bgDispatcher: CoroutineDispatcher,
logger: ConnectivityPipelineLogger,
+ mobileLogger: TableLogBuffer,
scope: CoroutineScope,
) : MobileConnectionRepository {
init {
@@ -81,10 +96,11 @@
private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+ override val tableLogBuffer: TableLogBuffer = mobileLogger
+
override val connectionInfo: StateFlow<MobileConnectionModel> = run {
var state = MobileConnectionModel()
conflatedCallbackFlow {
- // TODO (b/240569788): log all of these into the connectivity logger
val callback =
object :
TelephonyCallback(),
@@ -95,11 +111,18 @@
TelephonyCallback.CarrierNetworkListener,
TelephonyCallback.DisplayInfoListener {
override fun onServiceStateChanged(serviceState: ServiceState) {
- state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+ logger.logOnServiceStateChanged(serviceState, subId)
+ state =
+ state.copy(
+ isEmergencyOnly = serviceState.isEmergencyOnly,
+ isRoaming = serviceState.roaming,
+ operatorAlphaShort = serviceState.operatorAlphaShort,
+ )
trySend(state)
}
override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+ logger.logOnSignalStrengthsChanged(signalStrength, subId)
val cdmaLevel =
signalStrength
.getCellSignalStrengths(CellSignalStrengthCdma::class.java)
@@ -126,17 +149,23 @@
dataState: Int,
networkType: Int
) {
+ logger.logOnDataConnectionStateChanged(dataState, networkType, subId)
state =
state.copy(dataConnectionState = dataState.toDataConnectionType())
trySend(state)
}
override fun onDataActivity(direction: Int) {
- state = state.copy(dataActivityDirection = direction)
+ logger.logOnDataActivity(direction, subId)
+ state =
+ state.copy(
+ dataActivityDirection = direction.toMobileDataActivityModel()
+ )
trySend(state)
}
override fun onCarrierNetworkChange(active: Boolean) {
+ logger.logOnCarrierNetworkChange(active, subId)
state = state.copy(carrierNetworkChangeActive = active)
trySend(state)
}
@@ -144,6 +173,7 @@
override fun onDisplayInfoChanged(
telephonyDisplayInfo: TelephonyDisplayInfo
) {
+ logger.logOnDisplayInfoChanged(telephonyDisplayInfo, subId)
val networkType =
if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) {
@@ -174,7 +204,11 @@
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
.onEach { telephonyCallbackEvent.tryEmit(Unit) }
- .logOutputChange(logger, "MobileSubscriptionModel")
+ .logDiffsForTable(
+ mobileLogger,
+ columnPrefix = "MobileConnection ($subId)",
+ initialValue = state,
+ )
.stateIn(scope, SharingStarted.WhileSubscribed(), state)
}
@@ -208,46 +242,102 @@
globalMobileDataSettingChangedEvent,
)
- override val dataEnabled: StateFlow<Boolean> =
+ override val cdmaRoaming: StateFlow<Boolean> =
+ telephonyPollingEvent
+ .mapLatest { telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber != ERI_OFF }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val networkName: StateFlow<NetworkNameModel> =
+ broadcastDispatcher
+ .broadcastFlow(IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)) {
+ intent,
+ _ ->
+ if (intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, INVALID_SUBSCRIPTION_ID) != subId) {
+ defaultNetworkName
+ } else {
+ intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName
+ }
+ }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ mobileLogger,
+ columnPrefix = "",
+ initialValue = defaultNetworkName,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+
+ override val dataEnabled: StateFlow<Boolean> = run {
+ val initial = dataConnectionAllowed()
telephonyPollingEvent
.mapLatest { dataConnectionAllowed() }
- .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ mobileLogger,
+ columnPrefix = "",
+ columnName = "dataEnabled",
+ initialValue = initial,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
- override val isDefaultDataSubscription: StateFlow<Boolean> =
+ override val isDefaultDataSubscription: StateFlow<Boolean> = run {
+ val initialValue = defaultDataSubId.value == subId
defaultDataSubId
.mapLatest { it == subId }
- .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ mobileLogger,
+ columnPrefix = "",
+ columnName = "isDefaultDataSub",
+ initialValue = initialValue,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue)
+ }
class Factory
@Inject
constructor(
+ private val broadcastDispatcher: BroadcastDispatcher,
private val context: Context,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
private val globalSettings: GlobalSettings,
private val mobileMappingsProxy: MobileMappingsProxy,
+ private val logFactory: TableLogBufferFactory,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
) {
fun build(
subId: Int,
+ defaultNetworkName: NetworkNameModel,
+ networkNameSeparator: String,
defaultDataSubId: StateFlow<Int>,
globalMobileDataSettingChangedEvent: Flow<Unit>,
): MobileConnectionRepository {
+ val mobileLogger = logFactory.create(tableBufferLogName(subId), 100)
+
return MobileConnectionRepositoryImpl(
context,
subId,
+ defaultNetworkName,
+ networkNameSeparator,
telephonyManager.createForSubscriptionId(subId),
globalSettings,
+ broadcastDispatcher,
defaultDataSubId,
globalMobileDataSettingChangedEvent,
mobileMappingsProxy,
bgDispatcher,
logger,
+ mobileLogger,
scope,
)
}
}
+
+ companion object {
+ fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 483df47..d407abe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -38,17 +38,20 @@
import com.android.internal.telephony.PhoneConstants
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.R
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -88,6 +91,14 @@
) : MobileConnectionsRepository {
private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+ private val defaultNetworkName =
+ NetworkNameModel.Default(
+ context.getString(com.android.internal.R.string.lockscreen_carrier_default)
+ )
+
+ private val networkNameSeparator: String =
+ context.getString(R.string.status_bar_network_name_separator)
+
/**
* State flow that emits the set of mobile data subscriptions, each represented by its own
* [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
@@ -110,6 +121,7 @@
awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
}
.mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
+ .logInputChange(logger, "onSubscriptionsChanged")
.onEach { infos -> dropUnusedReposFromCache(infos) }
.stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
@@ -126,6 +138,8 @@
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
+ .distinctUntilChanged()
+ .logInputChange(logger, "onActiveDataSubscriptionIdChanged")
.stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
@@ -139,6 +153,7 @@
intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
}
.distinctUntilChanged()
+ .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED")
.onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
.stateIn(
scope,
@@ -147,13 +162,15 @@
)
private val carrierConfigChangedEvent =
- broadcastDispatcher.broadcastFlow(
- IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
- )
+ broadcastDispatcher
+ .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED))
+ .logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED")
override val defaultDataSubRatConfig: StateFlow<Config> =
merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
.mapLatest { Config.readConfig(context) }
+ .distinctUntilChanged()
+ .logInputChange(logger, "defaultDataSubRatConfig")
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
@@ -161,10 +178,16 @@
)
override val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> =
- defaultDataSubRatConfig.map { mobileMappingsProxy.mapIconSets(it) }
+ defaultDataSubRatConfig
+ .map { mobileMappingsProxy.mapIconSets(it) }
+ .distinctUntilChanged()
+ .logInputChange(logger, "defaultMobileIconMapping")
override val defaultMobileIconGroup: Flow<MobileIconGroup> =
- defaultDataSubRatConfig.map { mobileMappingsProxy.getDefaultIcons(it) }
+ defaultDataSubRatConfig
+ .map { mobileMappingsProxy.getDefaultIcons(it) }
+ .distinctUntilChanged()
+ .logInputChange(logger, "defaultMobileIconGroup")
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
if (!isValidSubId(subId)) {
@@ -181,22 +204,24 @@
* In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
* connection repositories also observe the URI for [MOBILE_DATA] + subId.
*/
- override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
- val observer =
- object : ContentObserver(null) {
- override fun onChange(selfChange: Boolean) {
- trySend(Unit)
- }
+ override val globalMobileDataSettingChangedEvent: Flow<Unit> =
+ conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ globalSettings.registerContentObserver(
+ globalSettings.getUriFor(MOBILE_DATA),
+ true,
+ observer
+ )
+
+ awaitClose { context.contentResolver.unregisterContentObserver(observer) }
}
-
- globalSettings.registerContentObserver(
- globalSettings.getUriFor(MOBILE_DATA),
- true,
- observer
- )
-
- awaitClose { context.contentResolver.unregisterContentObserver(observer) }
- }
+ .logInputChange(logger, "globalMobileDataSettingChangedEvent")
@SuppressLint("MissingPermission")
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
@@ -226,6 +251,8 @@
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
+ .distinctUntilChanged()
+ .logInputChange(logger, "defaultMobileNetworkConnectivity")
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
private fun isValidSubId(subId: Int): Boolean {
@@ -243,6 +270,8 @@
private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
return mobileConnectionRepositoryFactory.build(
subId,
+ defaultNetworkName,
+ networkNameSeparator,
defaultDataSubId,
globalMobileDataSettingChangedEvent,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index a26f28a..e6686dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -19,11 +19,15 @@
import android.telephony.CarrierConfigManager
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.util.CarrierConfigTracker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -32,6 +36,12 @@
import kotlinx.coroutines.flow.stateIn
interface MobileIconInteractor {
+ /** The table log created for this connection */
+ val tableLogBuffer: TableLogBuffer
+
+ /** The current mobile data activity */
+ val activity: Flow<DataActivityModel>
+
/** Only true if mobile is the default transport but is not validated, otherwise false */
val isDefaultConnectionFailed: StateFlow<Boolean>
@@ -51,9 +61,25 @@
/** Observable for RAT type (network type) indicator */
val networkTypeIconGroup: StateFlow<MobileIconGroup>
+ /**
+ * Provider name for this network connection. The name can be one of 3 values:
+ * 1. The default network name, if one is configured
+ * 2. A derived name based off of the intent [ACTION_SERVICE_PROVIDERS_UPDATED]
+ * 3. Or, in the case where the repository sends us the default network name, we check for an
+ * override in [connectionInfo.operatorAlphaShort], a value that is derived from [ServiceState]
+ */
+ val networkName: StateFlow<NetworkNameModel>
+
/** True if this line of service is emergency-only */
val isEmergencyOnly: StateFlow<Boolean>
+ /**
+ * True if this connection is considered roaming. The roaming bit can come from [ServiceState],
+ * or directly from the telephony manager's CDMA ERI number value. Note that we don't consider a
+ * connection to be roaming while carrier network change is active
+ */
+ val isRoaming: StateFlow<Boolean>
+
/** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
val level: StateFlow<Int>
@@ -75,10 +101,30 @@
) : MobileIconInteractor {
private val connectionInfo = connectionRepository.connectionInfo
+ override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer
+
+ override val activity = connectionInfo.mapLatest { it.dataActivityDirection }
+
override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
+ override val networkName =
+ combine(connectionInfo, connectionRepository.networkName) { connection, networkName ->
+ if (
+ networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null
+ ) {
+ NetworkNameModel.Derived(connection.operatorAlphaShort)
+ } else {
+ networkName
+ }
+ }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ connectionRepository.networkName.value
+ )
+
/** Observable for the current RAT indicator icon ([MobileIconGroup]) */
override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
combine(
@@ -95,6 +141,18 @@
.mapLatest { it.isEmergencyOnly }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ override val isRoaming: StateFlow<Boolean> =
+ combine(connectionInfo, connectionRepository.cdmaRoaming) { connection, cdmaRoaming ->
+ if (connection.carrierNetworkChangeActive) {
+ false
+ } else if (connection.isGsm) {
+ connection.isRoaming
+ } else {
+ cdmaRoaming
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
override val level: StateFlow<Int> =
connectionInfo
.mapLatest { connection ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index 62fa723..829a5ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -20,7 +20,6 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.phone.StatusBarIconController
-import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
@@ -70,6 +69,9 @@
private val mobileSubIdsState: StateFlow<List<Int>> =
mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+ /** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */
+ val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState)
+
override fun start() {
// Only notify the icon controller if we want to *render* the new icons.
// Note that this flow may still run if
@@ -81,12 +83,4 @@
}
}
}
-
- /**
- * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's
- * lifecycle. This will start collecting on [mobileSubIdsState] and link our new pipeline with
- * the old view system.
- */
- fun createMobileIconsViewModel(): MobileIconsViewModel =
- iconsViewModelFactory.create(mobileSubIdsState)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 67ea139..ab442b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -17,10 +17,12 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.binder
import android.content.res.ColorStateList
+import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.ImageView
+import android.widget.Space
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
@@ -28,8 +30,7 @@
import com.android.systemui.R
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
-import kotlinx.coroutines.flow.collect
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -38,11 +39,16 @@
@JvmStatic
fun bind(
view: ViewGroup,
- viewModel: MobileIconViewModel,
+ viewModel: LocationBasedMobileViewModel,
) {
+ val activityContainer = view.requireViewById<View>(R.id.inout_container)
+ val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)
+ val activityOut = view.requireViewById<ImageView>(R.id.mobile_out)
val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
+ val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
+ val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
view.isVisible = true
iconView.isVisible = true
@@ -64,12 +70,32 @@
}
}
+ // Set the roaming indicator
+ launch {
+ viewModel.roaming.distinctUntilChanged().collect { isRoaming ->
+ roamingView.isVisible = isRoaming
+ roamingSpace.isVisible = isRoaming
+ }
+ }
+
+ // Set the activity indicators
+ launch { viewModel.activityInVisible.collect { activityIn.isVisible = it } }
+
+ launch { viewModel.activityOutVisible.collect { activityOut.isVisible = it } }
+
+ launch {
+ viewModel.activityContainerVisible.collect { activityContainer.isVisible = it }
+ }
+
// Set the tint
launch {
viewModel.tint.collect { tint ->
val tintList = ColorStateList.valueOf(tint)
iconView.imageTintList = tintList
networkTypeView.imageTintList = tintList
+ roamingView.imageTintList = tintList
+ activityIn.imageTintList = tintList
+ activityOut.imageTintList = tintList
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
index 0ab7bcd..e86fee2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -24,7 +24,7 @@
import com.android.systemui.statusbar.BaseStatusBarFrameLayout
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
-import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
import java.util.ArrayList
class ModernStatusBarMobileView(
@@ -71,7 +71,7 @@
fun constructAndBind(
context: Context,
slot: String,
- viewModel: MobileIconViewModel,
+ viewModel: LocationBasedMobileViewModel,
): ModernStatusBarMobileView {
return (LayoutInflater.from(context)
.inflate(R.layout.status_bar_mobile_signal_group_new, null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
new file mode 100644
index 0000000..b0dc41f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 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.mobile.ui.viewmodel
+
+import android.graphics.Color
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This
+ * allows the mobile icon to change some view parameters at different locations
+ *
+ * @param commonImpl for convenience, this class wraps a base interface that can provides all of the
+ * common implementations between locations. See [MobileIconViewModel]
+ */
+abstract class LocationBasedMobileViewModel(
+ val commonImpl: MobileIconViewModelCommon,
+ val logger: ConnectivityPipelineLogger,
+) : MobileIconViewModelCommon by commonImpl {
+ abstract val tint: Flow<Int>
+
+ companion object {
+ fun viewModelForLocation(
+ commonImpl: MobileIconViewModelCommon,
+ logger: ConnectivityPipelineLogger,
+ loc: StatusBarLocation,
+ ): LocationBasedMobileViewModel =
+ when (loc) {
+ StatusBarLocation.HOME -> HomeMobileIconViewModel(commonImpl, logger)
+ StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl, logger)
+ StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, logger)
+ }
+ }
+}
+
+class HomeMobileIconViewModel(
+ commonImpl: MobileIconViewModelCommon,
+ logger: ConnectivityPipelineLogger,
+) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
+ override val tint: Flow<Int> =
+ flowOf(Color.CYAN)
+ .distinctUntilChanged()
+ .logOutputChange(logger, "HOME tint(${commonImpl.subscriptionId})")
+}
+
+class QsMobileIconViewModel(
+ commonImpl: MobileIconViewModelCommon,
+ logger: ConnectivityPipelineLogger,
+) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
+ override val tint: Flow<Int> =
+ flowOf(Color.GREEN)
+ .distinctUntilChanged()
+ .logOutputChange(logger, "QS tint(${commonImpl.subscriptionId})")
+}
+
+class KeyguardMobileIconViewModel(
+ commonImpl: MobileIconViewModelCommon,
+ logger: ConnectivityPipelineLogger,
+) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) {
+ override val tint: Flow<Int> =
+ flowOf(Color.MAGENTA)
+ .distinctUntilChanged()
+ .logOutputChange(logger, "KEYGUARD tint(${commonImpl.subscriptionId})")
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 8ebd718..2d6ac4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -16,20 +16,40 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
-import android.graphics.Color
import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/** Common interface for all of the location-based mobile icon view models. */
+interface MobileIconViewModelCommon {
+ val subscriptionId: Int
+ /** An int consumable by [SignalDrawable] for display */
+ val iconId: Flow<Int>
+ val roaming: Flow<Boolean>
+ /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
+ val networkTypeIcon: Flow<Icon?>
+ val activityInVisible: Flow<Boolean>
+ val activityOutVisible: Flow<Boolean>
+ val activityContainerVisible: Flow<Boolean>
+}
/**
* View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -37,24 +57,29 @@
* subscription's information.
*
* There will be exactly one [MobileIconViewModel] per filtered subscription offered from
- * [MobileIconsInteractor.filteredSubscriptions]
+ * [MobileIconsInteractor.filteredSubscriptions].
*
- * TODO: figure out where carrier merged and VCN models go (probably here?)
+ * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon]
+ * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view
+ * model gets the exact same information, as well as allows us to log that unified state only once
+ * per icon.
*/
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileIconViewModel
constructor(
- val subscriptionId: Int,
+ override val subscriptionId: Int,
iconInteractor: MobileIconInteractor,
logger: ConnectivityPipelineLogger,
-) {
+ constants: ConnectivityConstants,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommon {
/** Whether or not to show the error state of [SignalDrawable] */
private val showExclamationMark: Flow<Boolean> =
iconInteractor.isDefaultDataEnabled.mapLatest { !it }
- /** An int consumable by [SignalDrawable] for display */
- val iconId: Flow<Int> =
+ override val iconId: Flow<Int> = run {
+ val initial = SignalDrawable.getEmptyState(iconInteractor.numberOfLevels.value)
combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) {
level,
numberOfLevels,
@@ -62,30 +87,97 @@
SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
}
.distinctUntilChanged()
- .logOutputChange(logger, "iconId($subscriptionId)")
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "iconId",
+ initialValue = initial,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
- /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
- val networkTypeIcon: Flow<Icon?> =
+ override val networkTypeIcon: Flow<Icon?> =
combine(
- iconInteractor.networkTypeIconGroup,
- iconInteractor.isDataConnected,
- iconInteractor.isDataEnabled,
- iconInteractor.isDefaultConnectionFailed,
- iconInteractor.alwaysShowDataRatIcon,
- ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow ->
- val desc =
- if (networkTypeIconGroup.dataContentDescription != 0)
- ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
- else null
- val icon = Icon.Resource(networkTypeIconGroup.dataType, desc)
- return@combine when {
- alwaysShow -> icon
- !dataConnected -> null
- !dataEnabled -> null
- failedConnection -> null
- else -> icon
+ iconInteractor.networkTypeIconGroup,
+ iconInteractor.isDataConnected,
+ iconInteractor.isDataEnabled,
+ iconInteractor.isDefaultConnectionFailed,
+ iconInteractor.alwaysShowDataRatIcon,
+ ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow ->
+ val desc =
+ if (networkTypeIconGroup.dataContentDescription != 0)
+ ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
+ else null
+ val icon = Icon.Resource(networkTypeIconGroup.dataType, desc)
+ return@combine when {
+ alwaysShow -> icon
+ !dataConnected -> null
+ !dataEnabled -> null
+ failedConnection -> null
+ else -> icon
+ }
}
+ .distinctUntilChanged()
+ .onEach {
+ // This is done as an onEach side effect since Icon is not Diffable (yet)
+ iconInteractor.tableLogBuffer.logChange(
+ prefix = "",
+ columnName = "networkTypeIcon",
+ value = it.toString(),
+ )
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val roaming: StateFlow<Boolean> =
+ iconInteractor.isRoaming
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "roaming",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ private val activity: Flow<DataActivityModel?> =
+ if (!constants.shouldShowActivityConfig) {
+ flowOf(null)
+ } else {
+ iconInteractor.activity
}
- val tint: Flow<Int> = flowOf(Color.CYAN)
+ override val activityInVisible: Flow<Boolean> =
+ activity
+ .map { it?.hasActivityIn ?: false }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "activityInVisible",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activityOutVisible: Flow<Boolean> =
+ activity
+ .map { it?.hasActivityOut ?: false }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "activityOutVisible",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activityContainerVisible: Flow<Boolean> =
+ activity
+ .map { it != null && (it.hasActivityIn || it.hasActivityOut) }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnPrefix = "",
+ columnName = "activityContainerVisible",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index 2349cb7..b9318b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -14,16 +14,19 @@
* limitations under the License.
*/
-@file:OptIn(InternalCoroutinesApi::class)
-
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import javax.inject.Inject
-import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
/**
* View model for describing the system's current mobile cellular connections. The result is a list
@@ -36,26 +39,50 @@
val subscriptionIdsFlow: StateFlow<List<Int>>,
private val interactor: MobileIconsInteractor,
private val logger: ConnectivityPipelineLogger,
+ private val constants: ConnectivityConstants,
+ @Application private val scope: CoroutineScope,
) {
- /** TODO: do we need to cache these? */
- fun viewModelForSub(subId: Int): MobileIconViewModel =
- MobileIconViewModel(
- subId,
- interactor.createMobileConnectionInteractorForSubId(subId),
- logger
- )
+ @VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>()
+
+ init {
+ scope.launch { subscriptionIdsFlow.collect { removeInvalidModelsFromCache(it) } }
+ }
+
+ fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel {
+ val common =
+ mobileIconSubIdCache[subId]
+ ?: MobileIconViewModel(
+ subId,
+ interactor.createMobileConnectionInteractorForSubId(subId),
+ logger,
+ constants,
+ scope,
+ )
+ .also { mobileIconSubIdCache[subId] = it }
+
+ return LocationBasedMobileViewModel.viewModelForLocation(common, logger, location)
+ }
+
+ private fun removeInvalidModelsFromCache(subIds: List<Int>) {
+ val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) }
+ subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) }
+ }
class Factory
@Inject
constructor(
private val interactor: MobileIconsInteractor,
private val logger: ConnectivityPipelineLogger,
+ private val constants: ConnectivityConstants,
+ @Application private val scope: CoroutineScope,
) {
fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel {
return MobileIconsViewModel(
subscriptionIdsFlow,
interactor,
logger,
+ constants,
+ scope,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
index 6efb10f..0c9b86c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt
@@ -16,8 +16,10 @@
package com.android.systemui.statusbar.pipeline.shared
+import android.content.Context
import android.telephony.TelephonyManager
import com.android.systemui.Dumpable
+import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
@@ -32,15 +34,25 @@
@SysUISingleton
class ConnectivityConstants
@Inject
-constructor(dumpManager: DumpManager, telephonyManager: TelephonyManager) : Dumpable {
+constructor(
+ context: Context,
+ dumpManager: DumpManager,
+ telephonyManager: TelephonyManager,
+) : Dumpable {
init {
- dumpManager.registerDumpable("${SB_LOGGING_TAG}Constants", this)
+ dumpManager.registerNormalDumpable("${SB_LOGGING_TAG}Constants", this)
}
/** True if this device has the capability for data connections and false otherwise. */
val hasDataCapabilities = telephonyManager.isDataCapable
+ /** True if we should show the activityIn/activityOut icons and false otherwise */
+ val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity)
+
override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.apply { println("hasDataCapabilities=$hasDataCapabilities") }
+ pw.apply {
+ println("hasDataCapabilities=$hasDataCapabilities")
+ println("shouldShowActivityConfig=$shouldShowActivityConfig")
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index d3cf32f..d3ff357 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -18,8 +18,11 @@
import android.net.Network
import android.net.NetworkCapabilities
-import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.TelephonyDisplayInfo
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.StatusBarConnectivityLog
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
@@ -28,7 +31,9 @@
import kotlinx.coroutines.flow.onEach
@SysUISingleton
-class ConnectivityPipelineLogger @Inject constructor(
+class ConnectivityPipelineLogger
+@Inject
+constructor(
@StatusBarConnectivityLog private val buffer: LogBuffer,
) {
/**
@@ -37,34 +42,23 @@
* Use this method for inputs that don't have any extra information besides their callback name.
*/
fun logInputChange(callbackName: String) {
+ buffer.log(SB_LOGGING_TAG, LogLevel.INFO, { str1 = callbackName }, { "Input: $str1" })
+ }
+
+ /** Logs a change in one of the **raw inputs** to the connectivity pipeline. */
+ fun logInputChange(callbackName: String, changeInfo: String?) {
buffer.log(
SB_LOGGING_TAG,
LogLevel.INFO,
- { str1 = callbackName },
- { "Input: $str1" }
+ {
+ str1 = callbackName
+ str2 = changeInfo
+ },
+ { "Input: $str1: $str2" }
)
}
- /**
- * Logs a change in one of the **raw inputs** to the connectivity pipeline.
- */
- fun logInputChange(callbackName: String, changeInfo: String?) {
- buffer.log(
- SB_LOGGING_TAG,
- LogLevel.INFO,
- {
- str1 = callbackName
- str2 = changeInfo
- },
- {
- "Input: $str1: $str2"
- }
- )
- }
-
- /**
- * Logs a **data transformation** that we performed within the connectivity pipeline.
- */
+ /** Logs a **data transformation** that we performed within the connectivity pipeline. */
fun logTransformation(transformationName: String, oldValue: Any?, newValue: Any?) {
if (oldValue == newValue) {
buffer.log(
@@ -74,9 +68,7 @@
str1 = transformationName
str2 = oldValue.toString()
},
- {
- "Transform: $str1: $str2 (transformation didn't change it)"
- }
+ { "Transform: $str1: $str2 (transformation didn't change it)" }
)
} else {
buffer.log(
@@ -87,27 +79,21 @@
str2 = oldValue.toString()
str3 = newValue.toString()
},
- {
- "Transform: $str1: $str2 -> $str3"
- }
+ { "Transform: $str1: $str2 -> $str3" }
)
}
}
- /**
- * Logs a change in one of the **outputs** to the connectivity pipeline.
- */
+ /** Logs a change in one of the **outputs** to the connectivity pipeline. */
fun logOutputChange(outputParamName: String, changeInfo: String) {
buffer.log(
- SB_LOGGING_TAG,
- LogLevel.INFO,
- {
- str1 = outputParamName
- str2 = changeInfo
- },
- {
- "Output: $str1: $str2"
- }
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ str1 = outputParamName
+ str2 = changeInfo
+ },
+ { "Output: $str1: $str2" }
)
}
@@ -119,9 +105,7 @@
int1 = network.getNetId()
str1 = networkCapabilities.toString()
},
- {
- "onCapabilitiesChanged: net=$int1 capabilities=$str1"
- }
+ { "onCapabilitiesChanged: net=$int1 capabilities=$str1" }
)
}
@@ -129,21 +113,93 @@
buffer.log(
SB_LOGGING_TAG,
LogLevel.INFO,
+ { int1 = network.getNetId() },
+ { "onLost: net=$int1" }
+ )
+ }
+
+ fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
{
- int1 = network.getNetId()
+ int1 = subId
+ bool1 = serviceState.isEmergencyOnly
+ bool2 = serviceState.roaming
+ str1 = serviceState.operatorAlphaShort
},
{
- "onLost: net=$int1"
+ "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" +
+ " operator=$str1"
}
)
}
+ fun logOnSignalStrengthsChanged(signalStrength: SignalStrength, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ str1 = signalStrength.toString()
+ },
+ { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" }
+ )
+ }
+
+ fun logOnDataConnectionStateChanged(dataState: Int, networkType: Int, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ int2 = dataState
+ str1 = networkType.toString()
+ },
+ { "onDataConnectionStateChanged: subId=$int1 dataState=$int2 networkType=$str1" },
+ )
+ }
+
+ fun logOnDataActivity(direction: Int, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ int2 = direction
+ },
+ { "onDataActivity: subId=$int1 direction=$int2" },
+ )
+ }
+
+ fun logOnCarrierNetworkChange(active: Boolean, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ bool1 = active
+ },
+ { "onCarrierNetworkChange: subId=$int1 active=$bool1" },
+ )
+ }
+
+ fun logOnDisplayInfoChanged(displayInfo: TelephonyDisplayInfo, subId: Int) {
+ buffer.log(
+ SB_LOGGING_TAG,
+ LogLevel.INFO,
+ {
+ int1 = subId
+ str1 = displayInfo.toString()
+ },
+ { "onDisplayInfoChanged: subId=$int1 displayInfo=$str1" },
+ )
+ }
+
companion object {
const val SB_LOGGING_TAG = "SbConnectivity"
- /**
- * Log a change in one of the **inputs** to the connectivity pipeline.
- */
+ /** Log a change in one of the **inputs** to the connectivity pipeline. */
fun Flow<Unit>.logInputChange(
logger: ConnectivityPipelineLogger,
inputParamName: String,
@@ -155,26 +211,26 @@
* Log a change in one of the **inputs** to the connectivity pipeline.
*
* @param prettyPrint an optional function to transform the value into a readable string.
- * [toString] is used if no custom function is provided.
+ * [toString] is used if no custom function is provided.
*/
fun <T> Flow<T>.logInputChange(
logger: ConnectivityPipelineLogger,
inputParamName: String,
prettyPrint: (T) -> String = { it.toString() }
): Flow<T> {
- return this.onEach {logger.logInputChange(inputParamName, prettyPrint(it)) }
+ return this.onEach { logger.logInputChange(inputParamName, prettyPrint(it)) }
}
/**
* Log a change in one of the **outputs** to the connectivity pipeline.
*
* @param prettyPrint an optional function to transform the value into a readable string.
- * [toString] is used if no custom function is provided.
+ * [toString] is used if no custom function is provided.
*/
fun <T> Flow<T>.logOutputChange(
- logger: ConnectivityPipelineLogger,
- outputParamName: String,
- prettyPrint: (T) -> String = { it.toString() }
+ logger: ConnectivityPipelineLogger,
+ outputParamName: String,
+ prettyPrint: (T) -> String = { it.toString() }
): Flow<T> {
return this.onEach { logger.logOutputChange(outputParamName, prettyPrint(it)) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
new file mode 100644
index 0000000..05d0714
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 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.shared.data.model
+
+import android.net.wifi.WifiManager
+import android.telephony.Annotation
+import android.telephony.TelephonyManager
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+
+/** Provides information about the current data activity direction */
+data class DataActivityModel(
+ /** True if the connection has activity in (download). */
+ val hasActivityIn: Boolean,
+ /** True if the connection has activity out (upload). */
+ val hasActivityOut: Boolean,
+) : Diffable<DataActivityModel> {
+ override fun logDiffs(prevVal: DataActivityModel, row: TableRowLogger) {
+ if (prevVal.hasActivityIn != hasActivityIn) {
+ row.logChange(COL_ACTIVITY_IN, hasActivityIn)
+ }
+ if (prevVal.hasActivityOut != hasActivityOut) {
+ row.logChange(COL_ACTIVITY_OUT, hasActivityOut)
+ }
+ }
+
+ override fun logFull(row: TableRowLogger) {
+ row.logChange(COL_ACTIVITY_IN, hasActivityIn)
+ row.logChange(COL_ACTIVITY_OUT, hasActivityOut)
+ }
+}
+
+const val ACTIVITY_PREFIX = "dataActivity"
+private const val COL_ACTIVITY_IN = "in"
+private const val COL_ACTIVITY_OUT = "out"
+
+fun @receiver:Annotation.DataActivityType Int.toMobileDataActivityModel(): DataActivityModel =
+ when (this) {
+ TelephonyManager.DATA_ACTIVITY_IN ->
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+ TelephonyManager.DATA_ACTIVITY_OUT ->
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+ TelephonyManager.DATA_ACTIVITY_INOUT ->
+ DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+ else -> DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ }
+
+fun Int.toWifiDataActivityModel(): DataActivityModel =
+ when (this) {
+ WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN ->
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+ WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT ->
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+ WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT ->
+ DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+ else -> DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 0c9c1cc..5ccd6f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -42,9 +42,9 @@
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.ACTIVITY_PREFIX
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -74,7 +74,7 @@
val wifiNetwork: StateFlow<WifiNetworkModel>
/** Observable for the current wifi network activity. */
- val wifiActivity: StateFlow<WifiActivityModel>
+ val wifiActivity: StateFlow<DataActivityModel>
}
/** Real implementation of [WifiRepository]. */
@@ -230,7 +230,7 @@
initialValue = WIFI_NETWORK_DEFAULT
)
- override val wifiActivity: StateFlow<WifiActivityModel> =
+ override val wifiActivity: StateFlow<DataActivityModel> =
if (wifiManager == null) {
Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback")
flowOf(ACTIVITY_DEFAULT)
@@ -238,7 +238,7 @@
conflatedCallbackFlow {
val callback = TrafficStateCallback { state ->
logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
- trySend(trafficStateToWifiActivityModel(state))
+ trySend(state.toWifiDataActivityModel())
}
wifiManager.registerTrafficStateCallback(mainExecutor, callback)
awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
@@ -256,7 +256,9 @@
)
companion object {
- val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+ private const val ACTIVITY_PREFIX = "wifiActivity"
+
+ val ACTIVITY_DEFAULT = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
// Start out with no known wifi network.
// Note: [WifiStatusTracker] (the old implementation of connectivity logic) does do an
// initial fetch to get a starting wifi network. But, it uses a deprecated API
@@ -265,15 +267,6 @@
// NetworkCallback inside [wifiNetwork] for our wifi network information.
val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
- private fun trafficStateToWifiActivityModel(state: Int): WifiActivityModel {
- return WifiActivityModel(
- hasActivityIn = state == TrafficStateCallback.DATA_ACTIVITY_IN ||
- state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
- hasActivityOut = state == TrafficStateCallback.DATA_ACTIVITY_OUT ||
- state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
- )
- }
-
private fun networkCapabilitiesToWifiInfo(
networkCapabilities: NetworkCapabilities
): WifiInfo? {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
new file mode 100644
index 0000000..73bcdfd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 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.wifi.data.repository
+
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Provides the [WifiRepository] interface either through the [DemoWifiRepository] implementation,
+ * or the [WifiRepositoryImpl]'s prod implementation, based on the current demo mode value. In this
+ * way, downstream clients can all consist of real implementations and not care about which
+ * repository is responsible for the data. Graphically:
+ *
+ * ```
+ * RealRepository
+ * │
+ * ├──►RepositorySwitcher──►RealInteractor──►RealViewModel
+ * │
+ * DemoRepository
+ * ```
+ *
+ * When demo mode turns on, every flow will [flatMapLatest] to the current provider's version of
+ * that flow.
+ */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class WifiRepositorySwitcher
+@Inject
+constructor(
+ private val realImpl: WifiRepositoryImpl,
+ private val demoImpl: DemoWifiRepository,
+ private val demoModeController: DemoModeController,
+ @Application scope: CoroutineScope,
+) : WifiRepository {
+ private val isDemoMode =
+ conflatedCallbackFlow {
+ val callback =
+ object : DemoMode {
+ override fun dispatchDemoCommand(command: String?, args: Bundle?) {
+ // Don't care
+ }
+
+ override fun onDemoModeStarted() {
+ demoImpl.startProcessingCommands()
+ trySend(true)
+ }
+
+ override fun onDemoModeFinished() {
+ demoImpl.stopProcessingCommands()
+ trySend(false)
+ }
+ }
+
+ demoModeController.addCallback(callback)
+ awaitClose { demoModeController.removeCallback(callback) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
+
+ @VisibleForTesting
+ val activeRepo =
+ isDemoMode
+ .mapLatest { isDemoMode ->
+ if (isDemoMode) {
+ demoImpl
+ } else {
+ realImpl
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl)
+
+ override val isWifiEnabled: StateFlow<Boolean> =
+ activeRepo
+ .flatMapLatest { it.isWifiEnabled }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiEnabled.value)
+
+ override val isWifiDefault: StateFlow<Boolean> =
+ activeRepo
+ .flatMapLatest { it.isWifiDefault }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiDefault.value)
+
+ override val wifiNetwork: StateFlow<WifiNetworkModel> =
+ activeRepo
+ .flatMapLatest { it.wifiNetwork }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiNetwork.value)
+
+ override val wifiActivity: StateFlow<DataActivityModel> =
+ activeRepo
+ .flatMapLatest { it.wifiActivity }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiActivity.value)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
new file mode 100644
index 0000000..c588945
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 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.wifi.data.repository.demo
+
+import android.net.wifi.WifiManager
+import android.os.Bundle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+
+/** Data source to map between demo mode commands and inputs into [DemoWifiRepository]'s flows */
+@SysUISingleton
+class DemoModeWifiDataSource
+@Inject
+constructor(
+ demoModeController: DemoModeController,
+ @Application scope: CoroutineScope,
+) {
+ private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
+ private val _wifiCommands = demoCommandStream.map { args -> args.toWifiEvent() }
+ val wifiEvents = _wifiCommands.shareIn(scope, SharingStarted.WhileSubscribed())
+
+ private fun Bundle.toWifiEvent(): FakeWifiEventModel? {
+ val wifi = getString("wifi") ?: return null
+ return if (wifi == "show") {
+ activeWifiEvent()
+ } else {
+ FakeWifiEventModel.WifiDisabled
+ }
+ }
+
+ private fun Bundle.activeWifiEvent(): FakeWifiEventModel.Wifi {
+ val level = getString("level")?.toInt()
+ val activity = getString("activity")?.toActivity()
+ val ssid = getString("ssid")
+ val validated = getString("fully").toBoolean()
+
+ return FakeWifiEventModel.Wifi(
+ level = level,
+ activity = activity,
+ ssid = ssid,
+ validated = validated,
+ )
+ }
+
+ private fun String.toActivity(): Int =
+ when (this) {
+ "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT
+ "in" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN
+ "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT
+ else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
new file mode 100644
index 0000000..7890074
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 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.wifi.data.repository.demo
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/** Demo-able wifi repository to support SystemUI demo mode commands. */
+class DemoWifiRepository
+@Inject
+constructor(
+ private val dataSource: DemoModeWifiDataSource,
+ @Application private val scope: CoroutineScope,
+) : WifiRepository {
+ private var demoCommandJob: Job? = null
+
+ private val _isWifiEnabled = MutableStateFlow(false)
+ override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled
+
+ private val _isWifiDefault = MutableStateFlow(false)
+ override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault
+
+ private val _wifiNetwork = MutableStateFlow<WifiNetworkModel>(WifiNetworkModel.Inactive)
+ override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
+
+ private val _wifiActivity =
+ MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+ override val wifiActivity: StateFlow<DataActivityModel> = _wifiActivity
+
+ fun startProcessingCommands() {
+ demoCommandJob =
+ scope.launch {
+ dataSource.wifiEvents.filterNotNull().collect { event -> processEvent(event) }
+ }
+ }
+
+ fun stopProcessingCommands() {
+ demoCommandJob?.cancel()
+ }
+
+ private fun processEvent(event: FakeWifiEventModel) =
+ when (event) {
+ is FakeWifiEventModel.Wifi -> processEnabledWifiState(event)
+ is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState()
+ }
+
+ private fun processDisabledWifiState() {
+ _isWifiEnabled.value = false
+ _isWifiDefault.value = false
+ _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ _wifiNetwork.value = WifiNetworkModel.Inactive
+ }
+
+ private fun processEnabledWifiState(event: FakeWifiEventModel.Wifi) {
+ _isWifiEnabled.value = true
+ _isWifiDefault.value = true
+ _wifiActivity.value =
+ event.activity?.toWifiDataActivityModel()
+ ?: DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+ _wifiNetwork.value = event.toWifiNetworkModel()
+ }
+
+ private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel =
+ WifiNetworkModel.Active(
+ networkId = DEMO_NET_ID,
+ isValidated = validated ?: true,
+ level = level,
+ ssid = ssid,
+
+ // These fields below aren't supported in demo mode, since they aren't needed to satisfy
+ // the interface.
+ isPasspointAccessPoint = false,
+ isOnlineSignUpForPasspointAccessPoint = false,
+ passpointProviderFriendlyName = null,
+ )
+
+ companion object {
+ private const val DEMO_NET_ID = 1234
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
new file mode 100644
index 0000000..2353fb8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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.wifi.data.repository.demo.model
+
+/**
+ * Model for demo wifi commands, ported from [NetworkControllerImpl]
+ *
+ * Nullable fields represent optional command line arguments
+ */
+sealed interface FakeWifiEventModel {
+ data class Wifi(
+ val level: Int?,
+ val activity: Int?,
+ val ssid: String?,
+ val validated: Boolean?,
+ ) : FakeWifiEventModel
+
+ object WifiDisabled : FakeWifiEventModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index ec935fe..93041ce 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -19,10 +19,10 @@
import android.net.wifi.WifiManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -50,8 +50,8 @@
/** Our current wifi network. See [WifiNetworkModel]. */
val wifiNetwork: Flow<WifiNetworkModel>
- /** Our current wifi activity. See [WifiActivityModel]. */
- val activity: StateFlow<WifiActivityModel>
+ /** Our current wifi activity. See [DataActivityModel]. */
+ val activity: StateFlow<DataActivityModel>
/** True if we're configured to force-hide the wifi icon and false otherwise. */
val isForceHidden: Flow<Boolean>
@@ -82,7 +82,7 @@
override val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork
- override val activity: StateFlow<WifiActivityModel> = wifiRepository.wifiActivity
+ override val activity: StateFlow<DataActivityModel> = wifiRepository.wifiActivity
override val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map {
it.contains(ConnectivitySlot.WIFI)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
index 3c0eb91..4f7fe28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
@@ -38,16 +38,12 @@
dumpManager.registerDumpable("${SB_LOGGING_TAG}WifiConstants", this)
}
- /** True if we should show the activityIn/activityOut icons and false otherwise. */
- val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity)
-
/** True if we should always show the wifi icon when wifi is enabled and false otherwise. */
val alwaysShowIconIfEnabled =
context.resources.getBoolean(R.bool.config_showWifiIndicatorWhenEnabled)
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply {
- println("shouldShowActivityConfig=$shouldShowActivityConfig")
println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
deleted file mode 100644
index a4ca41c..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2022 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.wifi.shared.model
-
-import com.android.systemui.log.table.Diffable
-import com.android.systemui.log.table.TableRowLogger
-
-/** Provides information on the current wifi activity. */
-data class WifiActivityModel(
- /** True if the wifi has activity in (download). */
- val hasActivityIn: Boolean,
- /** True if the wifi has activity out (upload). */
- val hasActivityOut: Boolean,
-) : Diffable<WifiActivityModel> {
-
- override fun logDiffs(prevVal: WifiActivityModel, row: TableRowLogger) {
- if (prevVal.hasActivityIn != hasActivityIn) {
- row.logChange(COL_ACTIVITY_IN, hasActivityIn)
- }
- if (prevVal.hasActivityOut != hasActivityOut) {
- row.logChange(COL_ACTIVITY_OUT, hasActivityOut)
- }
- }
-
- override fun logFull(row: TableRowLogger) {
- row.logChange(COL_ACTIVITY_IN, hasActivityIn)
- row.logChange(COL_ACTIVITY_OUT, hasActivityOut)
- }
-}
-
-const val ACTIVITY_PREFIX = "wifiActivity"
-private const val COL_ACTIVITY_IN = "in"
-private const val COL_ACTIVITY_OUT = "out"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index ec7ba65..ab464cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -37,10 +37,10 @@
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -147,8 +147,8 @@
)
/** The wifi activity status. Null if we shouldn't display the activity status. */
- private val activity: Flow<WifiActivityModel?> =
- if (!wifiConstants.shouldShowActivityConfig) {
+ private val activity: Flow<DataActivityModel?> =
+ if (!connectivityConstants.shouldShowActivityConfig) {
flowOf(null)
} else {
combine(interactor.activity, interactor.ssid) { activity, ssid ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index d8a8c5d..c9ed0cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -47,6 +47,7 @@
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
+import android.view.ViewGroupOverlay;
import android.view.ViewRootImpl;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
@@ -57,7 +58,6 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -133,6 +133,7 @@
private RevealParams mRevealParams;
private Rect mContentBackgroundBounds;
private boolean mIsFocusAnimationFlagActive;
+ private boolean mIsAnimatingAppearance = false;
// TODO(b/193539698): move these to a Controller
private RemoteInputController mController;
@@ -142,10 +143,6 @@
private boolean mSending;
private NotificationViewWrapper mWrapper;
- private Integer mDefocusTargetHeight = null;
- private boolean mIsAnimatingAppearance = false;
-
-
// TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
// that need the controller shouldn't have access to the view
private RemoteInputViewController mViewController;
@@ -423,18 +420,6 @@
return mIsAnimatingAppearance;
}
- /**
- * View will ensure to use at most the provided defocusTargetHeight, when defocusing animated.
- * This is to ensure that the parent can resize itself to the targetHeight while the defocus
- * animation of the RemoteInputView is running.
- *
- * @param defocusTargetHeight The target height the parent will resize itself to. If null, the
- * RemoteInputView will not resize itself.
- */
- public void setDefocusTargetHeight(Integer defocusTargetHeight) {
- mDefocusTargetHeight = defocusTargetHeight;
- }
-
@VisibleForTesting
void onDefocus(boolean animate, boolean logClose) {
mController.removeRemoteInput(mEntry, mToken);
@@ -443,35 +428,28 @@
// During removal, we get reattached and lose focus. Not hiding in that
// case to prevent flicker.
if (!mRemoved) {
- if (animate && mIsFocusAnimationFlagActive) {
- Animator animator = getDefocusAnimator();
+ ViewGroup parent = (ViewGroup) getParent();
+ if (animate && parent != null && mIsFocusAnimationFlagActive) {
- // When defocusing, the notification needs to shrink. Therefore, we need to free
- // up the space that is needed for the RemoteInputView. This is done by setting
- // a negative top margin of the height difference of the RemoteInputView and its
- // sibling (the actions_container_layout containing the Reply button)
- if (mDefocusTargetHeight != null && mDefocusTargetHeight < getHeight()
- && mDefocusTargetHeight >= 0
- && getLayoutParams() instanceof FrameLayout.LayoutParams) {
- int heightToShrink = getHeight() - mDefocusTargetHeight;
- FrameLayout.LayoutParams layoutParams =
- (FrameLayout.LayoutParams) getLayoutParams();
- layoutParams.topMargin = -heightToShrink;
- setLayoutParams(layoutParams);
- ((ViewGroup) getParent().getParent()).setClipChildren(false);
- }
+ ViewGroup grandParent = (ViewGroup) parent.getParent();
+ ViewGroupOverlay overlay = parent.getOverlay();
+
+ // After adding this RemoteInputView to the overlay of the parent (and thus removing
+ // it from the parent itself), the parent will shrink in height. This causes the
+ // overlay to be moved. To correct the position of the overlay we need to offset it.
+ int overlayOffsetY = getMaxSiblingHeight() - getHeight();
+ overlay.add(this);
+ if (grandParent != null) grandParent.setClipChildren(false);
+
+ Animator animator = getDefocusAnimator(overlayOffsetY);
+ View self = this;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- //reset top margin after the animation
- if (getLayoutParams() instanceof FrameLayout.LayoutParams) {
- FrameLayout.LayoutParams layoutParams =
- (FrameLayout.LayoutParams) getLayoutParams();
- layoutParams.topMargin = 0;
- setLayoutParams(layoutParams);
- ((ViewGroup) getParent().getParent()).setClipChildren(true);
- }
+ overlay.remove(self);
+ parent.addView(self);
+ if (grandParent != null) grandParent.setClipChildren(true);
setVisibility(GONE);
if (mWrapper != null) {
mWrapper.setRemoteInputVisible(false);
@@ -609,7 +587,7 @@
}
/**
- * Sets whether the feature flag for the updated inline reply animation is active or not.
+ * Sets whether the feature flag for the revised inline reply animation is active or not.
* @param active
*/
public void setIsFocusAnimationFlagActive(boolean active) {
@@ -846,6 +824,23 @@
}
}
+ /**
+ * @return max sibling height (0 in case of no siblings)
+ */
+ public int getMaxSiblingHeight() {
+ ViewGroup parentView = (ViewGroup) getParent();
+ int maxHeight = 0;
+ if (parentView == null) return 0;
+ for (int i = 0; i < parentView.getChildCount(); i++) {
+ View siblingView = parentView.getChildAt(i);
+ if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight());
+ }
+ return maxHeight;
+ }
+
+ /**
+ * Creates an animator for the focus animation.
+ */
private Animator getFocusAnimator(View crossFadeView) {
final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f);
alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY);
@@ -854,7 +849,7 @@
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f);
scaleAnimator.addUpdateListener(valueAnimator -> {
- setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+ setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), 0);
});
scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
@@ -875,21 +870,26 @@
return animatorSet;
}
- private Animator getDefocusAnimator() {
+ /**
+ * Creates an animator for the defocus animation.
+ *
+ * @param offsetY The RemoteInputView will be offset by offsetY during the animation
+ */
+ private Animator getDefocusAnimator(int offsetY) {
final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f);
alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE);
scaleAnimator.addUpdateListener(valueAnimator -> {
- setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+ setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), offsetY);
});
scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
scaleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
- setFocusAnimationScaleY(1f);
+ setFocusAnimationScaleY(1f /* scaleY */, 0 /* verticalOffset */);
}
});
@@ -901,15 +901,21 @@
/**
* Sets affected view properties for a vertical scale animation
*
- * @param scaleY desired vertical view scale
+ * @param scaleY desired vertical view scale
+ * @param verticalOffset vertical offset to apply to the RemoteInputView during the animation
*/
- private void setFocusAnimationScaleY(float scaleY) {
+ private void setFocusAnimationScaleY(float scaleY, int verticalOffset) {
int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight());
- mContentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(),
+ Rect contentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(),
mContentView.getHeight() - verticalBoundOffset);
- mContentBackground.setBounds(mContentBackgroundBounds);
+ mContentBackground.setBounds(contentBackgroundBounds);
mContentView.setBackground(mContentBackground);
- setTranslationY(verticalBoundOffset);
+ if (scaleY == 1f) {
+ mContentBackgroundBounds = null;
+ } else {
+ mContentBackgroundBounds = contentBackgroundBounds;
+ }
+ setTranslationY(verticalBoundOffset + verticalOffset);
}
/** Handler for button click on send action in IME. */
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index db7315f..ad48e21 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -30,12 +30,16 @@
import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS
import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
+import androidx.annotation.CallSuper
import com.android.systemui.CoreStartable
+import com.android.systemui.Dumpable
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
import com.android.systemui.util.wakelock.WakeLock
+import java.io.PrintWriter
/**
* A generic controller that can temporarily display a new view in a new window.
@@ -69,11 +73,12 @@
@Main private val mainExecutor: DelayableExecutor,
private val accessibilityManager: AccessibilityManager,
private val configurationController: ConfigurationController,
+ private val dumpManager: DumpManager,
private val powerManager: PowerManager,
@LayoutRes private val viewLayoutRes: Int,
private val wakeLockBuilder: WakeLock.Builder,
private val systemClock: SystemClock,
-) : CoreStartable {
+) : CoreStartable, Dumpable {
/**
* Window layout params that will be used as a starting point for the [windowLayoutParams] of
* all subclasses.
@@ -109,6 +114,11 @@
return activeViews.getOrNull(0)
}
+ @CallSuper
+ override fun start() {
+ dumpManager.registerNormalDumpable(this)
+ }
+
/**
* Displays the view with the provided [newInfo].
*
@@ -321,7 +331,7 @@
return
}
- removeViewFromWindow(displayInfo)
+ removeViewFromWindow(displayInfo, removalReason)
// Prune anything that's already timed out before determining if we should re-display a
// different chipbar.
@@ -348,14 +358,14 @@
removeViewFromWindow(displayInfo)
}
- private fun removeViewFromWindow(displayInfo: DisplayInfo) {
+ private fun removeViewFromWindow(displayInfo: DisplayInfo, removalReason: String? = null) {
val view = displayInfo.view
if (view == null) {
logger.logViewRemovalIgnored(displayInfo.info.id, "View is null")
return
}
displayInfo.view = null // Need other places??
- animateViewOut(view) {
+ animateViewOut(view, removalReason) {
windowManager.removeView(view)
displayInfo.wakeLock?.release(displayInfo.info.wakeReason)
}
@@ -382,6 +392,19 @@
}
}
+ @Synchronized
+ @CallSuper
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println("Current time millis: ${systemClock.currentTimeMillis()}")
+ pw.println("Active views size: ${activeViews.size}")
+ activeViews.forEachIndexed { index, displayInfo ->
+ pw.println("View[$index]:")
+ pw.println(" info=${displayInfo.info}")
+ pw.println(" hasView=${displayInfo.view != null}")
+ pw.println(" timeExpiration=${displayInfo.timeExpirationMillis}")
+ }
+ }
+
/**
* A method implemented by subclasses to update [currentView] based on [newInfo].
*/
@@ -405,7 +428,11 @@
*
* @param onAnimationEnd an action that *must* be run once the animation finishes successfully.
*/
- internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ internal open fun animateViewOut(
+ view: ViewGroup,
+ removalReason: String? = null,
+ onAnimationEnd: Runnable
+ ) {
onAnimationEnd.run()
}
@@ -445,8 +472,6 @@
*/
var cancelViewTimeout: Runnable?,
)
-
- // TODO(b/258019006): Add a dump method that dumps the currently active views.
}
private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT"
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index f37ef82..52980c3 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -41,6 +41,7 @@
import com.android.systemui.common.ui.binder.TintedIconViewBinder
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -75,6 +76,7 @@
@Main mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
+ dumpManager: DumpManager,
powerManager: PowerManager,
private val falsingManager: FalsingManager,
private val falsingCollector: FalsingCollector,
@@ -89,6 +91,7 @@
mainExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
R.layout.chipbar,
wakeLockBuilder,
@@ -208,7 +211,7 @@
)
}
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val innerView = view.getInnerView()
innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
ViewHierarchyAnimator.animateRemoval(
@@ -225,8 +228,6 @@
return requireViewById(R.id.chipbar_inner)
}
- override fun start() {}
-
override fun getTouchableRegion(view: View, outRect: Rect) {
viewUtil.setRectToViewWindowLocation(view, outRect)
}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
index 1f44434..2464886 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java
@@ -33,6 +33,7 @@
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.demomode.DemoModeAvailabilityTracker;
import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.util.settings.GlobalSettings;
public class DemoModeFragment extends PreferenceFragment implements OnPreferenceChangeListener {
@@ -54,13 +55,15 @@
private SwitchPreference mOnSwitch;
private DemoModeController mDemoModeController;
+ private GlobalSettings mGlobalSettings;
private Tracker mDemoModeTracker;
// We are the only ones who ever call this constructor, so don't worry about the warning
@SuppressLint("ValidFragment")
- public DemoModeFragment(DemoModeController demoModeController) {
+ public DemoModeFragment(DemoModeController demoModeController, GlobalSettings globalSettings) {
super();
mDemoModeController = demoModeController;
+ mGlobalSettings = globalSettings;
}
@@ -80,7 +83,7 @@
screen.addPreference(mOnSwitch);
setPreferenceScreen(screen);
- mDemoModeTracker = new Tracker(context);
+ mDemoModeTracker = new Tracker(context, mGlobalSettings);
mDemoModeTracker.startTracking();
updateDemoModeEnabled();
updateDemoModeOn();
@@ -202,8 +205,8 @@
}
private class Tracker extends DemoModeAvailabilityTracker {
- Tracker(Context context) {
- super(context);
+ Tracker(Context context, GlobalSettings globalSettings) {
+ super(context, globalSettings);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
index 3231aec..32ecb67 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java
@@ -33,6 +33,7 @@
import com.android.systemui.R;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.util.settings.GlobalSettings;
import javax.inject.Inject;
@@ -44,12 +45,18 @@
private final DemoModeController mDemoModeController;
private final TunerService mTunerService;
+ private final GlobalSettings mGlobalSettings;
@Inject
- TunerActivity(DemoModeController demoModeController, TunerService tunerService) {
+ TunerActivity(
+ DemoModeController demoModeController,
+ TunerService tunerService,
+ GlobalSettings globalSettings
+ ) {
super();
mDemoModeController = demoModeController;
mTunerService = tunerService;
+ mGlobalSettings = globalSettings;
}
protected void onCreate(Bundle savedInstanceState) {
@@ -69,7 +76,7 @@
boolean showDemoMode = action != null && action.equals(
"com.android.settings.action.DEMO_MODE");
final PreferenceFragment fragment = showDemoMode
- ? new DemoModeFragment(mDemoModeController)
+ ? new DemoModeFragment(mDemoModeController, mGlobalSettings)
: new TunerFragment(mTunerService);
getFragmentManager().beginTransaction().replace(R.id.content_frame,
fragment, TAG_TUNER).commit();
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 9cca950..523cf68 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -159,18 +159,24 @@
ensureOverlayRemoved()
val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
- val newView =
- LightRevealScrim(context, null).apply {
- revealEffect = createLightRevealEffect()
- isScrimOpaqueChangedListener = Consumer {}
- revealAmount =
- when (reason) {
- FOLD -> TRANSPARENT
- UNFOLD -> BLACK
- }
- }
-
val params = getLayoutParams()
+ val newView =
+ LightRevealScrim(
+ context,
+ attrs = null,
+ initialWidth = params.width,
+ initialHeight = params.height
+ )
+ .apply {
+ revealEffect = createLightRevealEffect()
+ isScrimOpaqueChangedListener = Consumer {}
+ revealAmount =
+ when (reason) {
+ FOLD -> TRANSPARENT
+ UNFOLD -> BLACK
+ }
+ }
+
newRoot.setView(newView, params)
if (onOverlayReady != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 9653985..d6b3b22 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -21,6 +21,8 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.lifecycle.repeatWhenAttached
import java.util.function.Consumer
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
@@ -34,7 +36,10 @@
view: View,
flow: Flow<T>,
consumer: Consumer<T>,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
state: Lifecycle.State = Lifecycle.State.CREATED,
) {
- view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } }
+ view.repeatWhenAttached(coroutineContext) {
+ repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
index fa9bab2..1059543 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
@@ -150,10 +150,4 @@
getContext().getResources().getString(R.string.kg_prompt_reason_restart_password),
false);
}
-
- @Test
- public void testReset() {
- mKeyguardAbsKeyInputViewController.reset();
- verify(mKeyguardMessageAreaController).setMessage("", false);
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index c94c97c..be4bbdf 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -25,6 +25,7 @@
import android.testing.AndroidTestingRunner;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -59,6 +60,8 @@
@Mock
DozeParameters mDozeParameters;
@Mock
+ FeatureFlags mFeatureFlags;
+ @Mock
ScreenOffAnimationController mScreenOffAnimationController;
@Captor
private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
@@ -77,6 +80,7 @@
mKeyguardUpdateMonitor,
mConfigurationController,
mDozeParameters,
+ mFeatureFlags,
mScreenOffAnimationController);
}
@@ -96,9 +100,9 @@
public void setTranslationYExcludingMedia_forwardsCallToView() {
float translationY = 123f;
- mController.setTranslationYExcludingMedia(translationY);
+ mController.setTranslationY(translationY, /* excludeMedia= */true);
- verify(mKeyguardStatusView).setChildrenTranslationYExcludingMediaView(translationY);
+ verify(mKeyguardStatusView).setChildrenTranslationY(translationY, /* excludeMedia= */true);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
index ce44f4d..508aea5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
@@ -37,19 +37,23 @@
fun setChildrenTranslationYExcludingMediaView_mediaViewIsNotTranslated() {
val translationY = 1234f
- keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
+ keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */true)
assertThat(mediaView.translationY).isEqualTo(0)
- }
-
- @Test
- fun setChildrenTranslationYExcludingMediaView_childrenAreTranslated() {
- val translationY = 1234f
-
- keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
childrenExcludingMedia.forEach {
assertThat(it.translationY).isEqualTo(translationY)
}
}
-}
\ No newline at end of file
+
+ @Test
+ fun setChildrenTranslationYIncludeMediaView() {
+ val translationY = 1234f
+
+ keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */false)
+
+ statusViewContainer.children.forEach {
+ assertThat(it.translationY).isEqualTo(translationY)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
index 6c1f008..bb03f28 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
@@ -22,9 +22,13 @@
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -33,7 +37,6 @@
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
/**
@@ -50,7 +53,9 @@
@Mock private lateinit var parent: ViewGroup
- private lateinit var keyguardUnfoldTransition: KeyguardUnfoldTransition
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+
+ private lateinit var underTest: KeyguardUnfoldTransition
private lateinit var progressListener: TransitionProgressListener
private var xTranslationMax = 0f
@@ -61,10 +66,10 @@
xTranslationMax =
context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat()
- keyguardUnfoldTransition = KeyguardUnfoldTransition(context, progressProvider)
+ underTest = KeyguardUnfoldTransition(context, statusBarStateController, progressProvider)
- keyguardUnfoldTransition.setup(parent)
- keyguardUnfoldTransition.statusViewCentered = false
+ underTest.setup(parent)
+ underTest.statusViewCentered = false
verify(progressProvider).addCallback(capture(progressListenerCaptor))
progressListener = progressListenerCaptor.value
@@ -72,10 +77,11 @@
@Test
fun onTransition_centeredViewDoesNotMove() {
- keyguardUnfoldTransition.statusViewCentered = true
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+ underTest.statusViewCentered = true
val view = View(context)
- `when`(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
progressListener.onTransitionStarted()
assertThat(view.translationX).isZero()
@@ -89,4 +95,44 @@
progressListener.onTransitionFinished()
assertThat(view.translationX).isZero()
}
+
+ @Test
+ fun whenInShadeState_viewDoesNotMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInKeyguardState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
index e7e6918..bdd496e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java
@@ -18,6 +18,8 @@
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED;
+import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -32,6 +34,7 @@
import android.content.ClipboardManager;
import android.os.PersistableBundle;
import android.provider.DeviceConfig;
+import android.provider.Settings;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -66,6 +69,8 @@
@Mock
private ClipboardOverlayController mOverlayController;
@Mock
+ private ClipboardToast mClipboardToast;
+ @Mock
private UiEventLogger mUiEventLogger;
@Mock
private FeatureFlags mFeatureFlags;
@@ -84,6 +89,8 @@
@Spy
private Provider<ClipboardOverlayController> mOverlayControllerProvider;
+ private ClipboardListener mClipboardListener;
+
@Before
public void setup() {
@@ -93,7 +100,8 @@
when(mClipboardOverlayControllerLegacyFactory.create(any()))
.thenReturn(mOverlayControllerLegacy);
when(mClipboardManager.hasPrimaryClip()).thenReturn(true);
-
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 1);
mSampleClipData = new ClipData("Test", new String[]{"text/plain"},
new ClipData.Item("Test Item"));
@@ -101,16 +109,17 @@
when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource);
mDeviceConfigProxy = new DeviceConfigProxyFake();
+
+ mClipboardListener = new ClipboardListener(getContext(), mDeviceConfigProxy,
+ mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
+ mClipboardToast, mClipboardManager, mUiEventLogger, mFeatureFlags);
}
@Test
public void test_disabled() {
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"false", false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
+ mClipboardListener.start();
verifyZeroInteractions(mClipboardManager);
verifyZeroInteractions(mUiEventLogger);
}
@@ -119,10 +128,7 @@
public void test_enabled() {
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
+ mClipboardListener.start();
verify(mClipboardManager).addPrimaryClipChangedListener(any());
verifyZeroInteractions(mUiEventLogger);
}
@@ -133,11 +139,8 @@
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
- listener.onPrimaryClipChanged();
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
verify(mClipboardOverlayControllerLegacyFactory).create(any());
@@ -152,14 +155,14 @@
// Should clear the overlay controller
mRunnableCaptor.getValue().run();
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
// Not calling the runnable here, just change the clip again and verify that the overlay is
// NOT recreated.
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any());
verifyZeroInteractions(mOverlayControllerProvider);
@@ -171,11 +174,8 @@
mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED,
"true", false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
- listener.onPrimaryClipChanged();
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider).get();
@@ -190,14 +190,14 @@
// Should clear the overlay controller
mRunnableCaptor.getValue().run();
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider, times(2)).get();
// Not calling the runnable here, just change the clip again and verify that the overlay is
// NOT recreated.
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mOverlayControllerProvider, times(2)).get();
verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
@@ -233,13 +233,10 @@
public void test_logging_enterAndReenter() {
when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
+ mClipboardListener.start();
- listener.onPrimaryClipChanged();
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mUiEventLogger, times(1)).log(
ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
@@ -251,17 +248,29 @@
public void test_logging_enterAndReenter_new() {
when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true);
- ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy,
- mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory,
- mClipboardManager, mUiEventLogger, mFeatureFlags);
- listener.start();
+ mClipboardListener.start();
- listener.onPrimaryClipChanged();
- listener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
+ mClipboardListener.onPrimaryClipChanged();
verify(mUiEventLogger, times(1)).log(
ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource);
verify(mUiEventLogger, times(1)).log(
ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource);
}
+
+ @Test
+ public void test_userSetupIncomplete_showsToast() {
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0);
+
+ mClipboardListener.start();
+ mClipboardListener.onPrimaryClipChanged();
+
+ verify(mUiEventLogger, times(1)).log(
+ ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource);
+ verify(mClipboardToast, times(1)).showCopiedToast();
+ verifyZeroInteractions(mOverlayControllerProvider);
+ verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt
new file mode 100644
index 0000000..87c66b5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2022 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.demomode
+
+import android.content.Intent
+import android.os.Bundle
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.demomode.DemoMode.ACTION_DEMO
+import com.android.systemui.demomode.DemoMode.COMMAND_STATUS
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+@SmallTest
+class DemoModeControllerTest : SysuiTestCase() {
+ private lateinit var underTest: DemoModeController
+
+ @Mock private lateinit var dumpManager: DumpManager
+
+ private val globalSettings = FakeSettings()
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+
+ MockitoAnnotations.initMocks(this)
+
+ globalSettings.putInt(DemoModeController.DEMO_MODE_ALLOWED, 1)
+ globalSettings.putInt(DemoModeController.DEMO_MODE_ON, 1)
+
+ underTest =
+ DemoModeController(
+ context = context,
+ dumpManager = dumpManager,
+ globalSettings = globalSettings,
+ broadcastDispatcher = fakeBroadcastDispatcher
+ )
+
+ underTest.initialize()
+ }
+
+ @Test
+ fun `demo command flow - returns args`() =
+ testScope.runTest {
+ var latest: Bundle? = null
+ val flow = underTest.demoFlowForCommand(TEST_COMMAND)
+ val job = launch { flow.collect { latest = it } }
+
+ sendDemoCommand(args = mapOf("key1" to "val1"))
+ assertThat(latest!!.getString("key1")).isEqualTo("val1")
+
+ sendDemoCommand(args = mapOf("key2" to "val2"))
+ assertThat(latest!!.getString("key2")).isEqualTo("val2")
+
+ job.cancel()
+ }
+
+ private fun sendDemoCommand(command: String? = TEST_COMMAND, args: Map<String, String>) {
+ val intent = Intent(ACTION_DEMO)
+ intent.putExtra("command", command)
+ args.forEach { arg -> intent.putExtra(arg.key, arg.value) }
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { it.onReceive(context, intent) }
+ }
+
+ companion object {
+ // Use a valid command until we properly fake out everything
+ const val TEST_COMMAND = COMMAND_STATUS
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index 20d3cd5..d910a27 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -35,6 +35,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.display.AmbientDisplayConfiguration;
@@ -76,7 +77,8 @@
@RunWithLooper
@SmallTest
public class DozeSensorsTest extends SysuiTestCase {
-
+ @Mock
+ private Resources mResources;
@Mock
private AsyncSensorManager mSensorManager;
@Mock
@@ -426,7 +428,7 @@
@Test
public void testGesturesAllInitiallyRespectSettings() {
- DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters,
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
mProximitySensor, mFakeSettings, mAuthController,
mDevicePostureController, mUserTracker);
@@ -436,9 +438,47 @@
}
}
+ @Test
+ public void liftToWake_defaultSetting_configDefaultFalse() {
+ // WHEN the default lift to wake gesture setting is false
+ when(mResources.getBoolean(
+ com.android.internal.R.bool.config_dozePickupGestureEnabled)).thenReturn(false);
+
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
+ mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
+ mProximitySensor, mFakeSettings, mAuthController,
+ mDevicePostureController, mUserTracker);
+
+ for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
+ // THEN lift to wake's TriggerSensor enabledBySettings is false
+ if (sensor.mPulseReason == DozeLog.REASON_SENSOR_PICKUP) {
+ assertFalse(sensor.enabledBySetting());
+ }
+ }
+ }
+
+ @Test
+ public void liftToWake_defaultSetting_configDefaultTrue() {
+ // WHEN the default lift to wake gesture setting is true
+ when(mResources.getBoolean(
+ com.android.internal.R.bool.config_dozePickupGestureEnabled)).thenReturn(true);
+
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
+ mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
+ mProximitySensor, mFakeSettings, mAuthController,
+ mDevicePostureController, mUserTracker);
+
+ for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
+ // THEN lift to wake's TriggerSensor enabledBySettings is true
+ if (sensor.mPulseReason == DozeLog.REASON_SENSOR_PICKUP) {
+ assertTrue(sensor.enabledBySetting());
+ }
+ }
+ }
+
private class TestableDozeSensors extends DozeSensors {
TestableDozeSensors() {
- super(mSensorManager, mDozeParameters,
+ super(mResources, mSensorManager, mDozeParameters,
mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
mProximitySensor, mFakeSettings, mAuthController,
mDevicePostureController, mUserTracker);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
index 8e689cf..6c23254 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
@@ -1,18 +1,25 @@
package com.android.systemui.dreams
-import android.animation.Animator
import android.animation.AnimatorSet
import android.testing.AndroidTestingRunner
+import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dreams.complication.ComplicationHostViewController
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.statusbar.BlurUtils
-import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
@@ -28,14 +35,6 @@
private const val DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = 3L
private const val DREAM_IN_TRANSLATION_Y_DISTANCE = 6
private const val DREAM_IN_TRANSLATION_Y_DURATION = 7L
- private const val DREAM_OUT_TRANSLATION_Y_DISTANCE = 6
- private const val DREAM_OUT_TRANSLATION_Y_DURATION = 7L
- private const val DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM = 8L
- private const val DREAM_OUT_TRANSLATION_Y_DELAY_TOP = 9L
- private const val DREAM_OUT_ALPHA_DURATION = 10L
- private const val DREAM_OUT_ALPHA_DELAY_BOTTOM = 11L
- private const val DREAM_OUT_ALPHA_DELAY_TOP = 12L
- private const val DREAM_OUT_BLUR_DURATION = 13L
}
@Mock private lateinit var mockAnimator: AnimatorSet
@@ -43,6 +42,8 @@
@Mock private lateinit var hostViewController: ComplicationHostViewController
@Mock private lateinit var statusBarViewController: DreamOverlayStatusBarViewController
@Mock private lateinit var stateController: DreamOverlayStateController
+ @Mock private lateinit var configController: ConfigurationController
+ @Mock private lateinit var transitionViewModel: DreamingToLockscreenTransitionViewModel
private lateinit var controller: DreamOverlayAnimationsController
@Before
@@ -55,71 +56,48 @@
statusBarViewController,
stateController,
DREAM_BLUR_RADIUS,
+ transitionViewModel,
+ configController,
DREAM_IN_BLUR_ANIMATION_DURATION,
DREAM_IN_COMPLICATIONS_ANIMATION_DURATION,
DREAM_IN_TRANSLATION_Y_DISTANCE,
DREAM_IN_TRANSLATION_Y_DURATION,
- DREAM_OUT_TRANSLATION_Y_DISTANCE,
- DREAM_OUT_TRANSLATION_Y_DURATION,
- DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM,
- DREAM_OUT_TRANSLATION_Y_DELAY_TOP,
- DREAM_OUT_ALPHA_DURATION,
- DREAM_OUT_ALPHA_DELAY_BOTTOM,
- DREAM_OUT_ALPHA_DELAY_TOP,
- DREAM_OUT_BLUR_DURATION
)
+
+ val mockView: View = mock()
+ whenever(mockView.resources).thenReturn(mContext.resources)
+
+ runBlocking(Dispatchers.Main.immediate) { controller.init(mockView) }
}
@Test
- fun testExitAnimationOnEnd() {
- val mockCallback: () -> Unit = mock()
+ fun testWakeUpCallsExecutor() {
+ val mockExecutor: DelayableExecutor = mock()
+ val mockCallback: Runnable = mock()
- controller.startExitAnimations(
- view = mock(),
+ controller.wakeUp(
doneCallback = mockCallback,
- animatorBuilder = { mockAnimator }
+ executor = mockExecutor,
)
- val captor = argumentCaptor<Animator.AnimatorListener>()
- verify(mockAnimator).addListener(captor.capture())
- val listener = captor.value
-
- verify(mockCallback, never()).invoke()
- listener.onAnimationEnd(mockAnimator)
- verify(mockCallback, times(1)).invoke()
+ verify(mockExecutor).executeDelayed(eq(mockCallback), anyLong())
}
@Test
- fun testCancellation() {
- controller.startExitAnimations(
- view = mock(),
- doneCallback = mock(),
- animatorBuilder = { mockAnimator }
- )
-
- verify(mockAnimator, never()).cancel()
- controller.cancelAnimations()
- verify(mockAnimator, times(1)).cancel()
- }
-
- @Test
- fun testExitAfterStartWillCancel() {
+ fun testWakeUpAfterStartWillCancel() {
val mockStartAnimator: AnimatorSet = mock()
- val mockExitAnimator: AnimatorSet = mock()
- controller.startEntryAnimations(view = mock(), animatorBuilder = { mockStartAnimator })
+ controller.startEntryAnimations(animatorBuilder = { mockStartAnimator })
verify(mockStartAnimator, never()).cancel()
- controller.startExitAnimations(
- view = mock(),
+ controller.wakeUp(
doneCallback = mock(),
- animatorBuilder = { mockExitAnimator }
+ executor = mock(),
)
// Verify that we cancelled the start animator in favor of the exit
// animator.
verify(mockStartAnimator, times(1)).cancel()
- verify(mockExitAnimator, never()).cancel()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
new file mode 100644
index 0000000..9f534ef
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DreamOverlayCallbackControllerTest : SysuiTestCase() {
+
+ @Mock private lateinit var callback: DreamOverlayCallbackController.Callback
+
+ private lateinit var underTest: DreamOverlayCallbackController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = DreamOverlayCallbackController()
+ }
+
+ @Test
+ fun onWakeUpInvokesCallback() {
+ underTest.onStartDream()
+ assertThat(underTest.isDreaming).isEqualTo(true)
+
+ underTest.addCallback(callback)
+ underTest.onWakeUp()
+ verify(callback).onWakeUp()
+ assertThat(underTest.isDreaming).isEqualTo(false)
+
+ // Adding twice should not invoke twice
+ reset(callback)
+ underTest.addCallback(callback)
+ underTest.onWakeUp()
+ verify(callback, times(1)).onWakeUp()
+
+ // After remove, no call to callback
+ reset(callback)
+ underTest.removeCallback(callback)
+ underTest.onWakeUp()
+ verify(callback, never()).onWakeUp()
+ }
+
+ @Test
+ fun onStartDreamInvokesCallback() {
+ underTest.addCallback(callback)
+
+ assertThat(underTest.isDreaming).isEqualTo(false)
+
+ underTest.onStartDream()
+ verify(callback).onStartDream()
+ assertThat(underTest.isDreaming).isEqualTo(true)
+
+ // Adding twice should not invoke twice
+ reset(callback)
+ underTest.addCallback(callback)
+ underTest.onStartDream()
+ verify(callback, times(1)).onStartDream()
+
+ // After remove, no call to callback
+ reset(callback)
+ underTest.removeCallback(callback)
+ underTest.onStartDream()
+ verify(callback, never()).onStartDream()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 73c226d..2799a25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -203,7 +203,7 @@
mController.onViewAttached();
- verify(mAnimationsController).startEntryAnimations(mDreamOverlayContainerView);
+ verify(mAnimationsController).startEntryAnimations();
verify(mAnimationsController, never()).cancelAnimations();
}
@@ -213,7 +213,7 @@
mController.onViewAttached();
- verify(mAnimationsController, never()).startEntryAnimations(mDreamOverlayContainerView);
+ verify(mAnimationsController, never()).startEntryAnimations();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index ffb8342..4568d1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -113,6 +113,9 @@
@Mock
UiEventLogger mUiEventLogger;
+ @Mock
+ DreamOverlayCallbackController mDreamOverlayCallbackController;
+
@Captor
ArgumentCaptor<View> mViewCaptor;
@@ -141,7 +144,8 @@
mStateController,
mKeyguardUpdateMonitor,
mUiEventLogger,
- LOW_LIGHT_COMPONENT);
+ LOW_LIGHT_COMPONENT,
+ mDreamOverlayCallbackController);
}
@Test
@@ -353,6 +357,7 @@
mService.onWakeUp(callback);
mMainExecutor.runAllReady();
verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor);
+ verify(mDreamOverlayCallbackController).onWakeUp();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 5deac19..be712f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
import com.android.systemui.doze.DozeTransitionListener
+import com.android.systemui.dreams.DreamOverlayCallbackController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -66,6 +67,7 @@
@Mock private lateinit var dozeTransitionListener: DozeTransitionListener
@Mock private lateinit var authController: AuthController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController
private lateinit var underTest: KeyguardRepositoryImpl
@@ -83,6 +85,7 @@
keyguardUpdateMonitor,
dozeTransitionListener,
authController,
+ dreamOverlayCallbackController,
)
}
@@ -167,6 +170,29 @@
}
@Test
+ fun isKeyguardOccluded() =
+ runTest(UnconfinedTestDispatcher()) {
+ whenever(keyguardStateController.isOccluded).thenReturn(false)
+ var latest: Boolean? = null
+ val job = underTest.isKeyguardOccluded.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ val captor = argumentCaptor<KeyguardStateController.Callback>()
+ verify(keyguardStateController).addCallback(captor.capture())
+
+ whenever(keyguardStateController.isOccluded).thenReturn(true)
+ captor.value.onKeyguardShowingChanged()
+ assertThat(latest).isTrue()
+
+ whenever(keyguardStateController.isOccluded).thenReturn(false)
+ captor.value.onKeyguardShowingChanged()
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
fun isDozing() =
runTest(UnconfinedTestDispatcher()) {
var latest: Boolean? = null
@@ -318,7 +344,7 @@
}
@Test
- fun isDreaming() =
+ fun isDreamingFromKeyguardUpdateMonitor() =
runTest(UnconfinedTestDispatcher()) {
whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(false)
var latest: Boolean? = null
@@ -339,6 +365,29 @@
}
@Test
+ fun isDreamingFromDreamOverlayCallbackController() =
+ runTest(UnconfinedTestDispatcher()) {
+ whenever(dreamOverlayCallbackController.isDreaming).thenReturn(false)
+ var latest: Boolean? = null
+ val job = underTest.isDreamingWithOverlay.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ val listener =
+ withArgCaptor<DreamOverlayCallbackController.Callback> {
+ verify(dreamOverlayCallbackController).addCallback(capture())
+ }
+
+ listener.onStartDream()
+ assertThat(latest).isTrue()
+
+ listener.onWakeUp()
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
fun biometricUnlockState() =
runTest(UnconfinedTestDispatcher()) {
val values = mutableListOf<BiometricUnlockModel>()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index a6cf840..d2b7838 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -23,6 +23,8 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.WakeSleepReason
@@ -42,6 +44,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
+import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -64,8 +67,8 @@
// Used to verify transition requests for test output
@Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository
- private lateinit var lockscreenBouncerTransitionInteractor:
- LockscreenBouncerTransitionInteractor
+ private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
+ private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
@Before
fun setUp() {
@@ -79,25 +82,82 @@
transitionRepository = KeyguardTransitionRepositoryImpl()
runner = KeyguardTransitionRunner(transitionRepository)
- lockscreenBouncerTransitionInteractor =
- LockscreenBouncerTransitionInteractor(
+ fromLockscreenTransitionInteractor =
+ FromLockscreenTransitionInteractor(
scope = testScope,
keyguardInteractor = KeyguardInteractor(keyguardRepository),
shadeRepository = shadeRepository,
keyguardTransitionRepository = mockTransitionRepository,
keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
)
- lockscreenBouncerTransitionInteractor.start()
+ fromLockscreenTransitionInteractor.start()
+
+ fromDreamingTransitionInteractor =
+ FromDreamingTransitionInteractor(
+ scope = testScope,
+ keyguardInteractor = KeyguardInteractor(keyguardRepository),
+ keyguardTransitionRepository = mockTransitionRepository,
+ keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+ )
+ fromDreamingTransitionInteractor.start()
}
@Test
+ fun `DREAMING to LOCKSCREEN`() =
+ testScope.runTest {
+ // GIVEN a device is dreaming and occluded
+ keyguardRepository.setDreamingWithOverlay(true)
+ keyguardRepository.setKeyguardOccluded(true)
+ runCurrent()
+
+ // GIVEN a prior transition has run to DREAMING
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN doze is complete
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ // AND dreaming has stopped
+ keyguardRepository.setDreamingWithOverlay(false)
+ // AND occluded has stopped
+ keyguardRepository.setKeyguardOccluded(false)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to BOUNCER should occur
+ assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.DREAMING)
+ assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun `LOCKSCREEN to BOUNCER via bouncer showing call`() =
testScope.runTest {
// GIVEN a device that has at least woken up
keyguardRepository.setWakefulnessModel(startingToWake())
runCurrent()
- // GIVEN a transition has run to LOCKSCREEN
+ // GIVEN a prior transition has run to LOCKSCREEN
runner.startTransition(
testScope,
TransitionInfo(
@@ -122,7 +182,7 @@
verify(mockTransitionRepository).startTransition(capture())
}
// THEN a transition to BOUNCER should occur
- assertThat(info.ownerName).isEqualTo("LockscreenBouncerTransitionInteractor")
+ assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
assertThat(info.to).isEqualTo(KeyguardState.BOUNCER)
assertThat(info.animator).isNotNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..5571663
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: DreamingToLockscreenTransitionViewModel
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ val interactor = KeyguardTransitionInteractor(repository)
+ underTest = DreamingToLockscreenTransitionViewModel(interactor)
+ }
+
+ @Test
+ fun dreamOverlayTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+
+ // Only 3 values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(3)
+ assertThat(values[0])
+ .isEqualTo(
+ EMPHASIZED_ACCELERATE.getInterpolation(
+ animValue(0f, DREAM_OVERLAY_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[1])
+ .isEqualTo(
+ EMPHASIZED_ACCELERATE.getInterpolation(
+ animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[2])
+ .isEqualTo(
+ EMPHASIZED_ACCELERATE.getInterpolation(
+ animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
+ ) * pixels
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun dreamOverlayFadeOut() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.1f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+
+ // Only two values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(2)
+ assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
+ assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenFadeIn() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.1f))
+ // Should start running here...
+ repository.sendTransitionStep(step(0.2f))
+ repository.sendTransitionStep(step(0.3f))
+ // ...up to here
+ repository.sendTransitionStep(step(1f))
+
+ // Only two values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(2)
+ assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
+ assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+
+ assertThat(values.size).isEqualTo(4)
+ assertThat(values[0])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[1])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[2])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[3])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(1f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+
+ job.cancel()
+ }
+
+ private fun animValue(stepValue: Float, params: AnimationParams): Float {
+ val totalDuration = TO_LOCKSCREEN_DURATION
+ val startValue = (params.startTime / totalDuration).toFloat()
+
+ val multiplier = (totalDuration / params.duration).toFloat()
+ return (stepValue - startValue) * multiplier
+ }
+
+ private fun step(value: Float): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.LOCKSCREEN,
+ value = value,
+ transitionState = TransitionState.RUNNING,
+ ownerName = "DreamingToLockscreenTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..98d292d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: OccludedToLockscreenTransitionViewModel
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ val interactor = KeyguardTransitionInteractor(repository)
+ underTest = OccludedToLockscreenTransitionViewModel(interactor)
+ }
+
+ @Test
+ fun lockscreenFadeIn() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ // Should start running here...
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.4f))
+ repository.sendTransitionStep(step(0.5f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.6f))
+ repository.sendTransitionStep(step(1f))
+
+ // Only two values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(3)
+ assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+ assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA))
+ assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA))
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+
+ assertThat(values.size).isEqualTo(4)
+ assertThat(values[0])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[1])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[2])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[3])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(1f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+
+ job.cancel()
+ }
+
+ private fun animValue(stepValue: Float, params: AnimationParams): Float {
+ val totalDuration = TO_LOCKSCREEN_DURATION
+ val startValue = (params.startTime / totalDuration).toFloat()
+
+ val multiplier = (totalDuration / params.duration).toFloat()
+ return (stepValue - startValue) * multiplier
+ }
+
+ private fun step(value: Float): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.LOCKSCREEN,
+ value = value,
+ transitionState = TransitionState.RUNNING,
+ ownerName = "OccludedToLockscreenTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
index bad3f03..9c4e849 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
@@ -22,6 +22,7 @@
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
+import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.statusbar.CommandQueue
@@ -39,6 +40,7 @@
mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
+ dumpManager: DumpManager,
powerManager: PowerManager,
mainHandler: Handler,
mediaTttFlags: MediaTttFlags,
@@ -55,6 +57,7 @@
mainExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
mainHandler,
mediaTttFlags,
@@ -63,7 +66,7 @@
wakeLockBuilder,
systemClock,
) {
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
onAnimationEnd.run()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index ef0bfb7..cefc742 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -34,6 +34,7 @@
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.statusbar.CommandQueue
@@ -73,6 +74,8 @@
@Mock
private lateinit var configurationController: ConfigurationController
@Mock
+ private lateinit var dumpManager: DumpManager
+ @Mock
private lateinit var mediaTttFlags: MediaTttFlags
@Mock
private lateinit var powerManager: PowerManager
@@ -95,6 +98,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
+ whenever(mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()).thenReturn(true)
fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
@@ -122,6 +126,7 @@
fakeExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
Handler.getMain(),
mediaTttFlags,
@@ -150,6 +155,7 @@
FakeExecutor(FakeSystemClock()),
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
Handler.getMain(),
mediaTttFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index b03a545..4cc12c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -38,6 +38,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.dump.DumpManager
import com.android.systemui.media.taptotransfer.MediaTttFlags
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.plugins.FalsingManager
@@ -81,6 +82,7 @@
@Mock private lateinit var applicationInfo: ApplicationInfo
@Mock private lateinit var commandQueue: CommandQueue
@Mock private lateinit var configurationController: ConfigurationController
+ @Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var falsingManager: FalsingManager
@Mock private lateinit var falsingCollector: FalsingCollector
@Mock private lateinit var chipbarLogger: ChipbarLogger
@@ -137,6 +139,7 @@
fakeExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
falsingManager,
falsingCollector,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt
new file mode 100644
index 0000000..db6fc13
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Translates items away/towards the hinge when the device is opened/closed. This is controlled by
+ * the set of ids, which also dictact which direction to move and when, via a filter fn.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationPanelUnfoldAnimationControllerTest : SysuiTestCase() {
+
+ @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
+
+ @Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener>
+
+ @Mock private lateinit var parent: ViewGroup
+
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+
+ private lateinit var underTest: NotificationPanelUnfoldAnimationController
+ private lateinit var progressListener: TransitionProgressListener
+ private var xTranslationMax = 0f
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ xTranslationMax =
+ context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings).toFloat()
+
+ underTest =
+ NotificationPanelUnfoldAnimationController(
+ context,
+ statusBarStateController,
+ progressProvider
+ )
+ underTest.setup(parent)
+
+ verify(progressProvider).addCallback(capture(progressListenerCaptor))
+ progressListener = progressListenerCaptor.value
+ }
+
+ @Test
+ fun whenInKeyguardState_viewDoesNotMove() {
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInShadeState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInShadeLockedState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE_LOCKED)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 01f13e66..65b2ac0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -104,7 +104,10 @@
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
@@ -186,6 +189,8 @@
import java.util.List;
import java.util.Optional;
+import kotlinx.coroutines.CoroutineDispatcher;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -286,6 +291,10 @@
@Mock private ViewTreeObserver mViewTreeObserver;
@Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
@Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+ @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+ @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+ @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ @Mock private CoroutineDispatcher mMainDispatcher;
@Mock private MotionEvent mDownMotionEvent;
@Captor
private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
@@ -502,6 +511,10 @@
systemClock,
mKeyguardBottomAreaViewModel,
mKeyguardBottomAreaInteractor,
+ mDreamingToLockscreenTransitionViewModel,
+ mOccludedToLockscreenTransitionViewModel,
+ mMainDispatcher,
+ mKeyguardTransitionInteractor,
mDumpManager);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 43c6942..3e769e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -21,6 +21,7 @@
import android.provider.Settings.Secure.DOZE_TAP_SCREEN_GESTURE
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import android.os.PowerManager
import android.view.MotionEvent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -36,9 +37,9 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
-import org.mockito.ArgumentMatchers.anyObject
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito.never
@@ -106,7 +107,8 @@
underTest.onSingleTapUp(upEv)
// THEN wake up device if dozing
- verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces).wakeUpIfDozing(
+ anyLong(), any(), anyString(), eq(PowerManager.WAKE_REASON_TAP))
}
@Test
@@ -125,7 +127,8 @@
underTest.onDoubleTapEvent(upEv)
// THEN wake up device if dozing
- verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces).wakeUpIfDozing(
+ anyLong(), any(), anyString(), eq(PowerManager.WAKE_REASON_TAP))
}
@Test
@@ -156,7 +159,8 @@
underTest.onSingleTapUp(upEv)
// THEN the device doesn't wake up
- verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces, never()).wakeUpIfDozing(
+ anyLong(), any(), anyString(), anyInt())
}
@Test
@@ -203,7 +207,8 @@
underTest.onDoubleTapEvent(upEv)
// THEN the device doesn't wake up
- verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces, never()).wakeUpIfDozing(
+ anyLong(), any(), anyString(), anyInt())
}
@Test
@@ -222,7 +227,8 @@
underTest.onSingleTapUp(upEv)
// THEN the device doesn't wake up
- verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces, never()).wakeUpIfDozing(
+ anyLong(), any(), anyString(), anyInt())
}
@Test
@@ -241,7 +247,8 @@
underTest.onDoubleTapEvent(upEv)
// THEN the device doesn't wake up
- verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString())
+ verify(centralSurfaces, never()).wakeUpIfDozing(
+ anyLong(), any(), anyString(), anyInt())
}
fun updateSettings() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 99b58a3..8d96932 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -660,7 +660,6 @@
createController();
String message = mContext.getString(R.string.keyguard_retry);
when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
- when(mKeyguardUpdateMonitor.getIsFaceAuthenticated()).thenReturn(false);
when(mKeyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true);
mController.setVisible(true);
@@ -671,21 +670,6 @@
}
@Test
- public void transientIndication_swipeUpToRetry_faceAuthenticated() {
- createController();
- String message = mContext.getString(R.string.keyguard_retry);
- when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
- when(mKeyguardUpdateMonitor.getIsFaceAuthenticated()).thenReturn(true);
- when(mKeyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true);
-
- mController.setVisible(true);
- mController.getKeyguardCallback().onBiometricError(FACE_ERROR_TIMEOUT,
- "A message", BiometricSourceType.FACE);
-
- verify(mStatusBarKeyguardViewManager, never()).setKeyguardMessage(eq(message), any());
- }
-
- @Test
public void faceErrorTimeout_whenFingerprintEnrolled_doesNotShowMessage() {
createController();
when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
index 97fe25d..d3befb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
@@ -20,6 +20,7 @@
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -36,7 +37,7 @@
@Before
fun setUp() {
- scrim = LightRevealScrim(context, null)
+ scrim = LightRevealScrim(context, null, DEFAULT_WIDTH, DEFAULT_HEIGHT)
scrim.isScrimOpaqueChangedListener = Consumer { opaque ->
isOpaque = opaque
}
@@ -63,4 +64,25 @@
scrim.revealAmount = 0.5f
assertFalse("Scrim is opaque even though it's revealed", scrim.isScrimOpaque)
}
+
+ @Test
+ fun testBeforeOnMeasure_defaultDimensions() {
+ assertThat(scrim.viewWidth).isEqualTo(DEFAULT_WIDTH)
+ assertThat(scrim.viewHeight).isEqualTo(DEFAULT_HEIGHT)
+ }
+
+ @Test
+ fun testAfterOnMeasure_measuredDimensions() {
+ scrim.measure(/* widthMeasureSpec= */ exact(1), /* heightMeasureSpec= */ exact(2))
+
+ assertThat(scrim.viewWidth).isEqualTo(1)
+ assertThat(scrim.viewHeight).isEqualTo(2)
+ }
+
+ private fun exact(value: Int) = View.MeasureSpec.makeMeasureSpec(value, View.MeasureSpec.EXACTLY)
+
+ private companion object {
+ private const val DEFAULT_WIDTH = 42
+ private const val DEFAULT_HEIGHT = 24
+ }
}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 07ea630..7622549 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -20,6 +20,7 @@
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE;
+import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.RUBBER_BAND_FACTOR_NORMAL;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -47,6 +48,7 @@
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.testing.TestableResources;
import android.util.MathUtils;
import android.view.MotionEvent;
import android.view.View;
@@ -99,6 +101,7 @@
private NotificationStackScrollLayout mStackScroller; // Normally test this
private NotificationStackScrollLayout mStackScrollerInternal; // See explanation below
private AmbientState mAmbientState;
+ private TestableResources mTestableResources;
@Rule public MockitoRule mockito = MockitoJUnit.rule();
@Mock private CentralSurfaces mCentralSurfaces;
@@ -123,6 +126,7 @@
@UiThreadTest
public void setUp() throws Exception {
allowTestableLooperAsMainThread();
+ mTestableResources = mContext.getOrCreateTestableResources();
// Interact with real instance of AmbientState.
mAmbientState = spy(new AmbientState(
@@ -394,7 +398,7 @@
@Test
@UiThreadTest
public void testSetExpandedHeight_withSplitShade_doesntInterpolateStackHeight() {
- mContext.getOrCreateTestableResources()
+ mTestableResources
.addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
final int[] expectedStackHeight = {0};
@@ -405,12 +409,12 @@
.isEqualTo(expectedStackHeight[0]);
});
- mContext.getOrCreateTestableResources()
+ mTestableResources
.addOverride(R.bool.config_use_split_notification_shade, /* value= */ false);
expectedStackHeight[0] = 0;
mStackScroller.setExpandedHeight(100f);
- mContext.getOrCreateTestableResources()
+ mTestableResources
.addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
expectedStackHeight[0] = 100;
mStackScroller.setExpandedHeight(100f);
@@ -781,6 +785,39 @@
assertEquals(mAmbientState.getScrollY(), 0);
}
+ @Test
+ public void testSplitShade_hasTopOverscroll() {
+ mTestableResources
+ .addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
+ mStackScroller.updateSplitNotificationShade();
+ mAmbientState.setExpansionFraction(1f);
+
+ int topOverscrollPixels = 100;
+ mStackScroller.setOverScrolledPixels(topOverscrollPixels, true, false);
+
+ float expectedTopOverscrollAmount = topOverscrollPixels * RUBBER_BAND_FACTOR_NORMAL;
+ assertEquals(expectedTopOverscrollAmount, mStackScroller.getCurrentOverScrollAmount(true));
+ assertEquals(expectedTopOverscrollAmount, mAmbientState.getStackY());
+ }
+
+ @Test
+ public void testNormalShade_hasNoTopOverscroll() {
+ mTestableResources
+ .addOverride(R.bool.config_use_split_notification_shade, /* value= */ false);
+ mStackScroller.updateSplitNotificationShade();
+ mAmbientState.setExpansionFraction(1f);
+
+ int topOverscrollPixels = 100;
+ mStackScroller.setOverScrolledPixels(topOverscrollPixels, true, false);
+
+ float expectedTopOverscrollAmount = topOverscrollPixels * RUBBER_BAND_FACTOR_NORMAL;
+ assertEquals(expectedTopOverscrollAmount, mStackScroller.getCurrentOverScrollAmount(true));
+ // When not in split shade mode, then the overscroll effect is handled in
+ // NotificationPanelViewController and not in NotificationStackScrollLayout. Therefore
+ // mAmbientState must have stackY set to 0
+ assertEquals(0f, mAmbientState.getStackY());
+ }
+
private void setBarStateForTest(int state) {
// Can't inject this through the listener or we end up on the actual implementation
// rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index ae60c73..09254ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -31,6 +31,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
@@ -176,6 +177,8 @@
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.startingsurface.StartingSurface;
+import dagger.Lazy;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -188,8 +191,6 @@
import java.io.PrintWriter;
import java.util.Optional;
-import dagger.Lazy;
-
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper(setAsMainLooper = true)
@@ -304,6 +305,7 @@
@Mock private ViewRootImpl mViewRootImpl;
@Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher;
@Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
+ @Mock IPowerManager mPowerManagerService;
private ShadeController mShadeController;
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
@@ -317,9 +319,8 @@
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
- IPowerManager powerManagerService = mock(IPowerManager.class);
IThermalService thermalService = mock(IThermalService.class);
- mPowerManager = new PowerManager(mContext, powerManagerService, thermalService,
+ mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService,
Handler.createAsync(Looper.myLooper()));
mNotificationInterruptStateProvider =
@@ -361,7 +362,7 @@
when(mStackScrollerController.getView()).thenReturn(mStackScroller);
when(mStackScroller.generateLayoutParams(any())).thenReturn(new LayoutParams(0, 0));
when(mNotificationPanelView.getLayoutParams()).thenReturn(new LayoutParams(0, 0));
- when(powerManagerService.isInteractive()).thenReturn(true);
+ when(mPowerManagerService.isInteractive()).thenReturn(true);
when(mStackScroller.getActivatedChild()).thenReturn(null);
doAnswer(invocation -> {
@@ -1186,6 +1187,34 @@
verify(mStatusBarStateController).setState(SHADE);
}
+ @Test
+ public void dozing_wakeUp() throws RemoteException {
+ // GIVEN can wakeup when dozing & is dozing
+ when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true);
+ setDozing(true);
+
+ // WHEN wakeup is requested
+ final int wakeReason = PowerManager.WAKE_REASON_TAP;
+ mCentralSurfaces.wakeUpIfDozing(0, null, "", wakeReason);
+
+ // THEN power manager receives wakeup
+ verify(mPowerManagerService).wakeUp(eq(0L), eq(wakeReason), anyString(), anyString());
+ }
+
+ @Test
+ public void notDozing_noWakeUp() throws RemoteException {
+ // GIVEN can wakeup when dozing and NOT dozing
+ when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true);
+ setDozing(false);
+
+ // WHEN wakeup is requested
+ final int wakeReason = PowerManager.WAKE_REASON_TAP;
+ mCentralSurfaces.wakeUpIfDozing(0, null, "", wakeReason);
+
+ // THEN power manager receives wakeup
+ verify(mPowerManagerService, never()).wakeUp(anyLong(), anyInt(), anyString(), anyString());
+ }
+
/**
* Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard}
* to reconfigure the keyguard to reflect the requested showing/occluded states.
@@ -1222,6 +1251,13 @@
states);
}
+ private void setDozing(boolean isDozing) {
+ ArgumentCaptor<StatusBarStateController.StateListener> callbackCaptor =
+ ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+ verify(mStatusBarStateController).addCallback(callbackCaptor.capture(), anyInt());
+ callbackCaptor.getValue().onDozingChanged(isDozing);
+ }
+
public static class TestableNotificationInterruptStateProviderImpl extends
NotificationInterruptStateProviderImpl {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
new file mode 100644
index 0000000..f822ba0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 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.mobile.data.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableRowLogger
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CARRIER_NETWORK_CHANGE
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CDMA_LEVEL
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CONNECTION_STATE
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_EMERGENCY
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_IS_GSM
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_OPERATOR
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_RESOLVED_NETWORK_TYPE
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ROAMING
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+class MobileConnectionModelTest : SysuiTestCase() {
+
+ @Test
+ fun `log diff - initial log contains all columns`() {
+ val logger = TestLogger()
+ val connection = MobileConnectionModel()
+
+ connection.logFull(logger)
+
+ assertThat(logger.changes)
+ .contains(Pair(COL_EMERGENCY, connection.isEmergencyOnly.toString()))
+ assertThat(logger.changes).contains(Pair(COL_ROAMING, connection.isRoaming.toString()))
+ assertThat(logger.changes)
+ .contains(Pair(COL_OPERATOR, connection.operatorAlphaShort.toString()))
+ assertThat(logger.changes).contains(Pair(COL_IS_GSM, connection.isGsm.toString()))
+ assertThat(logger.changes).contains(Pair(COL_CDMA_LEVEL, connection.cdmaLevel.toString()))
+ assertThat(logger.changes)
+ .contains(Pair(COL_PRIMARY_LEVEL, connection.primaryLevel.toString()))
+ assertThat(logger.changes)
+ .contains(Pair(COL_CONNECTION_STATE, connection.dataConnectionState.toString()))
+ assertThat(logger.changes)
+ .contains(Pair(COL_ACTIVITY_DIRECTION, connection.dataActivityDirection.toString()))
+ assertThat(logger.changes)
+ .contains(
+ Pair(COL_CARRIER_NETWORK_CHANGE, connection.carrierNetworkChangeActive.toString())
+ )
+ assertThat(logger.changes)
+ .contains(Pair(COL_RESOLVED_NETWORK_TYPE, connection.resolvedNetworkType.toString()))
+ }
+
+ @Test
+ fun `log diff - primary level changes - only level is logged`() {
+ val logger = TestLogger()
+ val connectionOld = MobileConnectionModel(primaryLevel = 1)
+
+ val connectionNew = MobileConnectionModel(primaryLevel = 2)
+
+ connectionNew.logDiffs(connectionOld, logger)
+
+ assertThat(logger.changes).isEqualTo(listOf(Pair(COL_PRIMARY_LEVEL, "2")))
+ }
+
+ private class TestLogger : TableRowLogger {
+ val changes = mutableListOf<Pair<String, String>>()
+
+ override fun logChange(columnName: String, value: String?) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+
+ override fun logChange(columnName: String, value: Int) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+
+ override fun logChange(columnName: String, value: Boolean) {
+ changes.add(Pair(columnName, value.toString()))
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index 5265ec6..d6a9ee3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -16,11 +16,16 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import kotlinx.coroutines.flow.MutableStateFlow
// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository
-class FakeMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository {
+class FakeMobileConnectionRepository(
+ override val subId: Int,
+ override val tableLogBuffer: TableLogBuffer,
+) : MobileConnectionRepository {
private val _connectionInfo = MutableStateFlow(MobileConnectionModel())
override val connectionInfo = _connectionInfo
@@ -30,6 +35,11 @@
private val _isDefaultDataSubscription = MutableStateFlow(true)
override val isDefaultDataSubscription = _isDefaultDataSubscription
+ override val cdmaRoaming = MutableStateFlow(false)
+
+ override val networkName =
+ MutableStateFlow<NetworkNameModel>(NetworkNameModel.Default("default"))
+
fun setConnectionInfo(model: MobileConnectionModel) {
_connectionInfo.value = model
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 04d3cdd..7f93328 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -22,14 +22,17 @@
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import kotlinx.coroutines.flow.MutableStateFlow
// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository
-class FakeMobileConnectionsRepository(mobileMappings: MobileMappingsProxy) :
- MobileConnectionsRepository {
+class FakeMobileConnectionsRepository(
+ mobileMappings: MobileMappingsProxy,
+ val tableLogBuffer: TableLogBuffer,
+) : MobileConnectionsRepository {
val GSM_KEY = mobileMappings.toIconKey(GSM)
val LTE_KEY = mobileMappings.toIconKey(LTE)
val UMTS_KEY = mobileMappings.toIconKey(UMTS)
@@ -63,7 +66,7 @@
private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
return subIdRepos[subId]
- ?: FakeMobileConnectionRepository(subId).also { subIdRepos[subId] = it }
+ ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it }
}
private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index 18ae90d..5d377a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -24,6 +24,8 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource
@@ -37,6 +39,7 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -69,12 +72,14 @@
private lateinit var realRepo: MobileConnectionsRepositoryImpl
private lateinit var demoRepo: DemoMobileConnectionsRepository
private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
+ private lateinit var logFactory: TableLogBufferFactory
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var demoModeController: DemoModeController
+ @Mock private lateinit var dumpManager: DumpManager
private val globalSettings = FakeSettings()
private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
@@ -86,6 +91,8 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
+ logFactory = TableLogBufferFactory(dumpManager, FakeSystemClock())
+
// Never start in demo mode
whenever(demoModeController.isInDemoMode).thenReturn(false)
@@ -114,6 +121,7 @@
dataSource = mockDataSource,
scope = scope,
context = context,
+ logFactory = logFactory,
)
underTest =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index e943de2..2102085 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -18,15 +18,20 @@
import android.telephony.Annotation
import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import androidx.test.filters.SmallTest
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
@@ -51,6 +56,9 @@
@RunWith(Parameterized::class)
internal class DemoMobileConnectionParameterizedTest(private val testCase: TestCase) :
SysuiTestCase() {
+
+ private val logFactory = TableLogBufferFactory(mock(), FakeSystemClock())
+
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -73,6 +81,7 @@
dataSource = mockDataSource,
scope = testScope.backgroundScope,
context = context,
+ logFactory = logFactory,
)
connectionsRepo.startProcessingCommands()
@@ -95,6 +104,8 @@
inflateStrength = testCase.inflateStrength,
activity = testCase.activity,
carrierNetworkChange = testCase.carrierNetworkChange,
+ roaming = testCase.roaming,
+ name = "demo name",
)
fakeNetworkEventFlow.value = networkModel
@@ -113,9 +124,12 @@
assertThat(conn.subId).isEqualTo(model.subId)
assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
- assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
+ assertThat(connectionInfo.dataActivityDirection)
+ .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel())
assertThat(connectionInfo.carrierNetworkChangeActive)
.isEqualTo(model.carrierNetworkChange)
+ assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
+ assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
// TODO(b/261029387): check these once we start handling them
assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -138,6 +152,8 @@
val inflateStrength: Boolean,
@Annotation.DataActivityType val activity: Int,
val carrierNetworkChange: Boolean,
+ val roaming: Boolean,
+ val name: String,
) {
override fun toString(): String {
return "INPUT(level=$level, " +
@@ -146,7 +162,9 @@
"carrierId=$carrierId, " +
"inflateStrength=$inflateStrength, " +
"activity=$activity, " +
- "carrierNetworkChange=$carrierNetworkChange)"
+ "carrierNetworkChange=$carrierNetworkChange, " +
+ "roaming=$roaming, " +
+ "name=$name)"
}
// Convenience for iterating test data and creating new cases
@@ -158,6 +176,8 @@
inflateStrength: Boolean? = null,
@Annotation.DataActivityType activity: Int? = null,
carrierNetworkChange: Boolean? = null,
+ roaming: Boolean? = null,
+ name: String? = null,
): TestCase =
TestCase(
level = level ?: this.level,
@@ -166,7 +186,9 @@
carrierId = carrierId ?: this.carrierId,
inflateStrength = inflateStrength ?: this.inflateStrength,
activity = activity ?: this.activity,
- carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange
+ carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange,
+ roaming = roaming ?: this.roaming,
+ name = name ?: this.name,
)
}
@@ -193,6 +215,9 @@
TelephonyManager.DATA_ACTIVITY_INOUT
)
private val carrierNetworkChange = booleanList
+ // false first so the base case doesn't have roaming set (more common)
+ private val roaming = listOf(false, true)
+ private val names = listOf("name 1", "name 2")
@Parameters(name = "{0}") @JvmStatic fun data() = testData()
@@ -226,7 +251,9 @@
carrierIds.first(),
inflateStrength.first(),
activity.first(),
- carrierNetworkChange.first()
+ carrierNetworkChange.first(),
+ roaming.first(),
+ names.first(),
)
val tail =
@@ -237,6 +264,8 @@
inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
activity.map { baseCase.modifiedBy(activity = it) },
carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
+ roaming.map { baseCase.modifiedBy(roaming = it) },
+ names.map { baseCase.modifiedBy(name = it) },
)
.flatten()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index 32d0410..cdbe75e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -17,18 +17,24 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
import androidx.test.filters.SmallTest
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons.THREE_G
import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -44,6 +50,9 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class DemoMobileConnectionsRepositoryTest : SysuiTestCase() {
+ private val dumpManager: DumpManager = mock()
+ private val logFactory = TableLogBufferFactory(dumpManager, FakeSystemClock())
+
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -65,6 +74,7 @@
dataSource = mockDataSource,
scope = testScope.backgroundScope,
context = context,
+ logFactory = logFactory,
)
underTest.startProcessingCommands()
@@ -289,9 +299,12 @@
assertThat(conn.subId).isEqualTo(model.subId)
assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level)
assertThat(connectionInfo.primaryLevel).isEqualTo(model.level)
- assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity)
+ assertThat(connectionInfo.dataActivityDirection)
+ .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel())
assertThat(connectionInfo.carrierNetworkChangeActive)
.isEqualTo(model.carrierNetworkChange)
+ assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
+ assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
// TODO(b/261029387) check these once we start handling them
assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -313,6 +326,7 @@
inflateStrength: Boolean? = false,
activity: Int? = null,
carrierNetworkChange: Boolean = false,
+ roaming: Boolean = false,
): FakeNetworkEventModel =
FakeNetworkEventModel.Mobile(
level = level,
@@ -322,4 +336,6 @@
inflateStrength = inflateStrength,
activity = activity,
carrierNetworkChange = carrierNetworkChange,
+ roaming = roaming,
+ name = "demo name",
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 1fc9c60..7970443 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+import android.content.Intent
import android.os.UserHandle
import android.provider.Settings
import android.telephony.CellSignalStrengthCdma
@@ -23,27 +24,45 @@
import android.telephony.SignalStrength
import android.telephony.SubscriptionInfo
import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.DataActivityListener
import android.telephony.TelephonyCallback.ServiceStateListener
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.DATA_ACTIVITY_DORMANT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_IN
+import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT
+import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
+import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
import android.telephony.TelephonyManager.DATA_CONNECTED
import android.telephony.TelephonyManager.DATA_CONNECTING
import android.telephony.TelephonyManager.DATA_DISCONNECTED
import android.telephony.TelephonyManager.DATA_DISCONNECTING
import android.telephony.TelephonyManager.DATA_UNKNOWN
+import android.telephony.TelephonyManager.ERI_OFF
+import android.telephony.TelephonyManager.ERI_ON
+import android.telephony.TelephonyManager.EXTRA_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+import android.telephony.TelephonyManager.EXTRA_SPN
+import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
@@ -69,14 +88,15 @@
@SmallTest
class MobileConnectionRepositoryTest : SysuiTestCase() {
private lateinit var underTest: MobileConnectionRepositoryImpl
+ private lateinit var connectionsRepo: FakeMobileConnectionsRepository
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var tableLogger: TableLogBuffer
private val scope = CoroutineScope(IMMEDIATE)
private val mobileMappings = FakeMobileMappingsProxy()
private val globalSettings = FakeSettings()
- private val connectionsRepo = FakeMobileConnectionsRepository(mobileMappings)
@Before
fun setUp() {
@@ -84,17 +104,23 @@
globalSettings.userId = UserHandle.USER_ALL
whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+ connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger)
+
underTest =
MobileConnectionRepositoryImpl(
context,
SUB_1_ID,
+ DEFAULT_NAME,
+ SEP,
telephonyManager,
globalSettings,
+ fakeBroadcastDispatcher,
connectionsRepo.defaultDataSubId,
connectionsRepo.globalMobileDataSettingChangedEvent,
mobileMappings,
IMMEDIATE,
logger,
+ tableLogger,
scope,
)
}
@@ -247,10 +273,11 @@
var latest: MobileConnectionModel? = null
val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
- val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>()
- callback.onDataActivity(3)
+ val callback = getTelephonyCallbackForType<DataActivityListener>()
+ callback.onDataActivity(DATA_ACTIVITY_INOUT)
- assertThat(latest?.dataActivityDirection).isEqualTo(3)
+ assertThat(latest?.dataActivityDirection)
+ .isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel())
job.cancel()
}
@@ -402,6 +429,167 @@
job.cancel()
}
+ @Test
+ fun `roaming - cdma - queries telephony manager`() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ // Start the telephony collection job so that cdmaRoaming starts updating
+ val telephonyJob = underTest.connectionInfo.launchIn(this)
+ val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
+
+ val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+ val serviceState = ServiceState()
+ serviceState.roaming = false
+
+ // CDMA roaming is off, GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ cb.onServiceStateChanged(serviceState)
+
+ assertThat(latest).isFalse()
+
+ // CDMA roaming is off, GSM roaming is on
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_ON)
+ cb.onServiceStateChanged(serviceState)
+
+ assertThat(latest).isTrue()
+
+ telephonyJob.cancel()
+ job.cancel()
+ }
+
+ @Test
+ fun `roaming - gsm - queries service state`() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.connectionInfo.onEach { latest = it.isRoaming }.launchIn(this)
+
+ val serviceState = ServiceState()
+ serviceState.roaming = false
+
+ val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+ // CDMA roaming is off, GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ cb.onServiceStateChanged(serviceState)
+
+ assertThat(latest).isFalse()
+
+ // CDMA roaming is off, GSM roaming is on
+ serviceState.roaming = true
+ cb.onServiceStateChanged(serviceState)
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `activity - updates from callback`() =
+ runBlocking(IMMEDIATE) {
+ var latest: DataActivityModel? = null
+ val job =
+ underTest.connectionInfo.onEach { latest = it.dataActivityDirection }.launchIn(this)
+
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ val cb = getTelephonyCallbackForType<DataActivityListener>()
+ cb.onDataActivity(DATA_ACTIVITY_IN)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false))
+
+ cb.onDataActivity(DATA_ACTIVITY_OUT)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true))
+
+ cb.onDataActivity(DATA_ACTIVITY_INOUT)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+ cb.onDataActivity(DATA_ACTIVITY_NONE)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ cb.onDataActivity(DATA_ACTIVITY_DORMANT)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ cb.onDataActivity(1234)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - default`() =
+ runBlocking(IMMEDIATE) {
+ var latest: NetworkNameModel? = null
+ val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - uses broadcast info - returns derived`() =
+ runBlocking(IMMEDIATE) {
+ var latest: NetworkNameModel? = null
+ val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+ val intent = spnIntent()
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(context, intent)
+ }
+
+ assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - broadcast not for this sub id - returns default`() =
+ runBlocking(IMMEDIATE) {
+ var latest: NetworkNameModel? = null
+ val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+ val intent = spnIntent(subId = 101)
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(context, intent)
+ }
+
+ assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - operatorAlphaShort - tracked`() =
+ runBlocking(IMMEDIATE) {
+ var latest: String? = null
+
+ val job =
+ underTest.connectionInfo.onEach { latest = it.operatorAlphaShort }.launchIn(this)
+
+ val shortName = "short name"
+ val serviceState = ServiceState()
+ serviceState.setOperatorName(
+ /* longName */ "long name",
+ /* shortName */ shortName,
+ /* numeric */ "12345",
+ )
+
+ getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+ assertThat(latest).isEqualTo(shortName)
+
+ job.cancel()
+ }
+
private fun getTelephonyCallbacks(): List<TelephonyCallback> {
val callbackCaptor = argumentCaptor<TelephonyCallback>()
Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
@@ -427,10 +615,31 @@
return signalStrength
}
+ private fun spnIntent(
+ subId: Int = SUB_1_ID,
+ showSpn: Boolean = true,
+ spn: String = SPN,
+ showPlmn: Boolean = true,
+ plmn: String = PLMN,
+ ): Intent =
+ Intent(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED).apply {
+ putExtra(EXTRA_SUBSCRIPTION_ID, subId)
+ putExtra(EXTRA_SHOW_SPN, showSpn)
+ putExtra(EXTRA_SPN, spn)
+ putExtra(EXTRA_SHOW_PLMN, showPlmn)
+ putExtra(EXTRA_PLMN, plmn)
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
private const val SUB_1_ID = 1
private val SUB_1 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+ private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+ private const val SEP = "-"
+
+ private const val SPN = "testSpn"
+ private const val PLMN = "testPlmn"
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 6d80acb..b8cd7a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -34,12 +34,15 @@
import com.android.settingslib.R
import com.android.settingslib.mobile.MobileMappings
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
@@ -57,6 +60,7 @@
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -72,6 +76,7 @@
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var logBufferFactory: TableLogBufferFactory
private val mobileMappings = FakeMobileMappingsProxy()
@@ -89,8 +94,13 @@
}
}
+ whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ ->
+ mock<TableLogBuffer>()
+ }
+
connectionFactory =
MobileConnectionRepositoryImpl.Factory(
+ fakeBroadcastDispatcher,
context = context,
telephonyManager = telephonyManager,
bgDispatcher = IMMEDIATE,
@@ -98,6 +108,7 @@
logger = logger,
mobileMappingsProxy = mobileMappings,
scope = scope,
+ logFactory = logBufferFactory,
)
underTest =
@@ -270,6 +281,32 @@
}
@Test
+ fun `connection repository - log buffer contains sub id in its name`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.subscriptions.launchIn(this)
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(SUB_1, SUB_2))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // Get repos to trigger creation
+ underTest.getRepoForSubId(SUB_1_ID)
+ verify(logBufferFactory)
+ .create(
+ eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)),
+ anyInt(),
+ )
+ underTest.getRepoForSubId(SUB_2_ID)
+ verify(logBufferFactory)
+ .create(
+ eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)),
+ anyInt(),
+ )
+
+ job.cancel()
+ }
+
+ @Test
fun testDefaultDataSubId_updatesOnBroadcast() =
runBlocking(IMMEDIATE) {
var latest: Int? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 1ff1636a..c494589 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -19,17 +19,34 @@
import android.telephony.CellSignalStrength
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeMobileIconInteractor : MobileIconInteractor {
+class FakeMobileIconInteractor(
+ override val tableLogBuffer: TableLogBuffer,
+) : MobileIconInteractor {
override val alwaysShowDataRatIcon = MutableStateFlow(false)
+ override val activity =
+ MutableStateFlow(
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ )
+ )
+
private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
override val networkTypeIconGroup = _iconGroup
+ override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo mode"))
+
private val _isEmergencyOnly = MutableStateFlow(false)
override val isEmergencyOnly = _isEmergencyOnly
+ override val isRoaming = MutableStateFlow(false)
+
private val _isFailedConnection = MutableStateFlow(false)
override val isDefaultConnectionFailed = _isFailedConnection
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 9f300e9..19e5516 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -22,12 +22,15 @@
import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor {
+class FakeMobileIconsInteractor(
+ mobileMappings: MobileMappingsProxy,
+ val tableLogBuffer: TableLogBuffer,
+) : MobileIconsInteractor {
val THREE_G_KEY = mobileMappings.toIconKey(THREE_G)
val LTE_KEY = mobileMappings.toIconKey(LTE)
val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G)
@@ -48,8 +51,7 @@
override val isDefaultConnectionFailed = MutableStateFlow(false)
- private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
- override val filteredSubscriptions: Flow<List<SubscriptionModel>> = _filteredSubscriptions
+ override val filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false)
override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled
@@ -67,7 +69,7 @@
/** Always returns a new fake interactor */
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
- return FakeMobileIconInteractor()
+ return FakeMobileIconInteractor(tableLogBuffer)
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 2281e89b..83c5055 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
@@ -48,8 +49,8 @@
class MobileIconInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconInteractor
private val mobileMappingsProxy = FakeMobileMappingsProxy()
- private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
- private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID)
+ private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock())
+ private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID, mock())
private val scope = CoroutineScope(IMMEDIATE)
@@ -298,6 +299,135 @@
job.cancel()
}
+ @Test
+ fun `roaming - is gsm - uses connection model`() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+
+ connectionRepository.cdmaRoaming.value = true
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ isGsm = true,
+ isRoaming = false,
+ )
+ )
+ yield()
+
+ assertThat(latest).isFalse()
+
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ isGsm = true,
+ isRoaming = true,
+ )
+ )
+ yield()
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `roaming - is cdma - uses cdma roaming bit`() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+
+ connectionRepository.cdmaRoaming.value = false
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ isGsm = false,
+ isRoaming = true,
+ )
+ )
+ yield()
+
+ assertThat(latest).isFalse()
+
+ connectionRepository.cdmaRoaming.value = true
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ isGsm = false,
+ isRoaming = false,
+ )
+ )
+ yield()
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `roaming - false while carrierNetworkChangeActive`() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+
+ connectionRepository.cdmaRoaming.value = true
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ isGsm = false,
+ isRoaming = true,
+ carrierNetworkChangeActive = true,
+ )
+ )
+ yield()
+
+ assertThat(latest).isFalse()
+
+ connectionRepository.cdmaRoaming.value = true
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(
+ isGsm = true,
+ isRoaming = true,
+ carrierNetworkChangeActive = true,
+ )
+ )
+ yield()
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `network name - uses operatorAlphaShot when non null and repo is default`() =
+ runBlocking(IMMEDIATE) {
+ var latest: NetworkNameModel? = null
+ val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+ val testOperatorName = "operatorAlphaShort"
+
+ // Default network name, operator name is non-null, uses the operator name
+ connectionRepository.networkName.value = DEFAULT_NAME
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(operatorAlphaShort = testOperatorName)
+ )
+ yield()
+
+ assertThat(latest).isEqualTo(NetworkNameModel.Derived(testOperatorName))
+
+ // Default network name, operator name is null, uses the default
+ connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null))
+ yield()
+
+ assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+ // Derived network name, operator name non-null, uses the derived name
+ connectionRepository.networkName.value = DERIVED_NAME
+ connectionRepository.setConnectionInfo(
+ MobileConnectionModel(operatorAlphaShort = testOperatorName)
+ )
+ yield()
+
+ assertThat(latest).isEqualTo(DERIVED_NAME)
+
+ job.cancel()
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
@@ -307,5 +437,8 @@
private const val SUB_1_ID = 1
private val SUB_1 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+ private val DEFAULT_NAME = NetworkNameModel.Default("test default name")
+ private val DERIVED_NAME = NetworkNameModel.Derived("test derived name")
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 8557894..2fa3467 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -20,6 +20,7 @@
import androidx.test.filters.SmallTest
import com.android.settingslib.mobile.MobileMappings
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
@@ -28,6 +29,7 @@
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -44,9 +46,9 @@
@SmallTest
class MobileIconsInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsInteractor
+ private lateinit var connectionsRepository: FakeMobileConnectionsRepository
private val userSetupRepository = FakeUserSetupRepository()
private val mobileMappingsProxy = FakeMobileMappingsProxy()
- private val connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy)
private val scope = CoroutineScope(IMMEDIATE)
@Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
@@ -55,6 +57,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
+ connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer)
connectionsRepository.setMobileConnectionRepositoryMap(
mapOf(
SUB_1_ID to CONNECTION_1,
@@ -290,21 +293,23 @@
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
+ private val tableLogBuffer =
+ TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock())
private const val SUB_1_ID = 1
private val SUB_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
- private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID)
+ private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer)
private const val SUB_2_ID = 2
private val SUB_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
- private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID)
+ private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, tableLogBuffer)
private const val SUB_3_ID = 3
private val SUB_3_OPP = SubscriptionModel(subscriptionId = SUB_3_ID, isOpportunistic = true)
- private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID)
+ private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, tableLogBuffer)
private const val SUB_4_ID = 4
private val SUB_4_OPP = SubscriptionModel(subscriptionId = SUB_4_ID, isOpportunistic = true)
- private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID)
+ private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, tableLogBuffer)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
new file mode 100644
index 0000000..043d55a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 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.mobile.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class LocationBasedMobileIconViewModelTest : SysuiTestCase() {
+ private lateinit var commonImpl: MobileIconViewModelCommon
+ private lateinit var homeIcon: HomeMobileIconViewModel
+ private lateinit var qsIcon: QsMobileIconViewModel
+ private lateinit var keyguardIcon: KeyguardMobileIconViewModel
+ private lateinit var interactor: FakeMobileIconInteractor
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var constants: ConnectivityConstants
+ @Mock private lateinit var tableLogBuffer: TableLogBuffer
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ interactor = FakeMobileIconInteractor(tableLogBuffer)
+ interactor.apply {
+ setLevel(1)
+ setIsDefaultDataEnabled(true)
+ setIsFailedConnection(false)
+ setIconGroup(TelephonyIcons.THREE_G)
+ setIsEmergencyOnly(false)
+ setNumberOfLevels(4)
+ isDataConnected.value = true
+ }
+ commonImpl =
+ MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
+
+ homeIcon = HomeMobileIconViewModel(commonImpl, logger)
+ qsIcon = QsMobileIconViewModel(commonImpl, logger)
+ keyguardIcon = KeyguardMobileIconViewModel(commonImpl, logger)
+ }
+
+ @Test
+ fun `location based view models receive same icon id when common impl updates`() =
+ testScope.runTest {
+ var latestHome: Int? = null
+ val homeJob = homeIcon.iconId.onEach { latestHome = it }.launchIn(this)
+
+ var latestQs: Int? = null
+ val qsJob = qsIcon.iconId.onEach { latestQs = it }.launchIn(this)
+
+ var latestKeyguard: Int? = null
+ val keyguardJob = keyguardIcon.iconId.onEach { latestKeyguard = it }.launchIn(this)
+
+ var expected = defaultSignal(level = 1)
+
+ assertThat(latestHome).isEqualTo(expected)
+ assertThat(latestQs).isEqualTo(expected)
+ assertThat(latestKeyguard).isEqualTo(expected)
+
+ interactor.setLevel(2)
+ expected = defaultSignal(level = 2)
+
+ assertThat(latestHome).isEqualTo(expected)
+ assertThat(latestQs).isEqualTo(expected)
+ assertThat(latestKeyguard).isEqualTo(expected)
+
+ homeJob.cancel()
+ qsJob.cancel()
+ keyguardJob.cancel()
+ }
+
+ companion object {
+ private const val SUB_1_ID = 1
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index f2533a9..50221bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -22,28 +22,42 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class MobileIconViewModelTest : SysuiTestCase() {
private lateinit var underTest: MobileIconViewModel
- private val interactor = FakeMobileIconInteractor()
+ private lateinit var interactor: FakeMobileIconInteractor
@Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var constants: ConnectivityConstants
+ @Mock private lateinit var tableLogBuffer: TableLogBuffer
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ interactor = FakeMobileIconInteractor(tableLogBuffer)
interactor.apply {
setLevel(1)
setIsDefaultDataEnabled(true)
@@ -53,12 +67,13 @@
setNumberOfLevels(4)
isDataConnected.value = true
}
- underTest = MobileIconViewModel(SUB_1_ID, interactor, logger)
+ underTest =
+ MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
}
@Test
fun iconId_correctLevel_notCutout() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
var latest: Int? = null
val job = underTest.iconId.onEach { latest = it }.launchIn(this)
val expected = defaultSignal()
@@ -70,7 +85,7 @@
@Test
fun iconId_cutout_whenDefaultDataDisabled() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIsDefaultDataEnabled(false)
var latest: Int? = null
@@ -84,7 +99,7 @@
@Test
fun networkType_dataEnabled_groupIsRepresented() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val expected =
Icon.Resource(
THREE_G.dataType,
@@ -102,7 +117,7 @@
@Test
fun networkType_nullWhenDisabled() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.setIsDataEnabled(false)
var latest: Icon? = null
@@ -115,7 +130,7 @@
@Test
fun networkType_nullWhenFailedConnection() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.setIsDataEnabled(true)
interactor.setIsFailedConnection(true)
@@ -129,7 +144,7 @@
@Test
fun networkType_nullWhenDataDisconnects() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val initial =
Icon.Resource(
THREE_G.dataType,
@@ -153,7 +168,7 @@
@Test
fun networkType_null_changeToDisabled() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
val expected =
Icon.Resource(
THREE_G.dataType,
@@ -176,7 +191,7 @@
@Test
fun networkType_alwaysShow_shownEvenWhenDisabled() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.setIsDataEnabled(true)
interactor.alwaysShowDataRatIcon.value = true
@@ -196,7 +211,7 @@
@Test
fun networkType_alwaysShow_shownEvenWhenDisconnected() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.isDataConnected.value = false
interactor.alwaysShowDataRatIcon.value = true
@@ -216,7 +231,7 @@
@Test
fun networkType_alwaysShow_shownEvenWhenFailedConnection() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.setIsFailedConnection(true)
interactor.alwaysShowDataRatIcon.value = true
@@ -234,16 +249,131 @@
job.cancel()
}
- /** Convenience constructor for these tests */
- private fun defaultSignal(
- level: Int = 1,
- connected: Boolean = true,
- ): Int {
- return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
- }
+ @Test
+ fun roaming() =
+ testScope.runTest {
+ interactor.isRoaming.value = true
+ var latest: Boolean? = null
+ val job = underTest.roaming.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ interactor.isRoaming.value = false
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `data activity - null when config is off`() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(false)
+ underTest =
+ MobileIconViewModel(
+ SUB_1_ID,
+ interactor,
+ logger,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityInVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this)
+
+ interactor.activity.value =
+ DataActivityModel(
+ hasActivityIn = true,
+ hasActivityOut = true,
+ )
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isFalse()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
+
+ @Test
+ fun `data activity - config on - test indicators`() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(true)
+ underTest =
+ MobileIconViewModel(
+ SUB_1_ID,
+ interactor,
+ logger,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
+
+ interactor.activity.value =
+ DataActivityModel(
+ hasActivityIn = true,
+ hasActivityOut = false,
+ )
+
+ yield()
+
+ assertThat(inVisible).isTrue()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isTrue()
+
+ interactor.activity.value =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = true,
+ )
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isTrue()
+ assertThat(containerVisible).isTrue()
+
+ interactor.activity.value =
+ DataActivityModel(
+ hasActivityIn = false,
+ hasActivityOut = false,
+ )
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isFalse()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
private const val SUB_1_ID = 1
+
+ /** Convenience constructor for these tests */
+ fun defaultSignal(
+ level: Int = 1,
+ connected: Boolean = true,
+ ): Int {
+ return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
+ }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
new file mode 100644
index 0000000..d6cb762
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 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.mobile.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+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.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileIconsViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: MobileIconsViewModel
+ private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var constants: ConnectivityConstants
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ val subscriptionIdsFlow =
+ interactor.filteredSubscriptions
+ .map { subs -> subs.map { it.subscriptionId } }
+ .stateIn(testScope.backgroundScope, SharingStarted.WhileSubscribed(), listOf())
+
+ underTest =
+ MobileIconsViewModel(
+ subscriptionIdsFlow,
+ interactor,
+ logger,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ }
+
+ @Test
+ fun `caching - mobile icon view model is reused for same sub id`() =
+ testScope.runTest {
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(1, StatusBarLocation.QS)
+
+ assertThat(model1.commonImpl).isSameInstanceAs(model2.commonImpl)
+ }
+
+ @Test
+ fun `caching - invalid view models are removed from cache when sub disappears`() =
+ testScope.runTest {
+ // Retrieve models to trigger caching
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS)
+
+ // Both impls are cached
+ assertThat(underTest.mobileIconSubIdCache)
+ .containsExactly(1, model1.commonImpl, 2, model2.commonImpl)
+
+ // SUB_1 is removed from the list...
+ interactor.filteredSubscriptions.value = listOf(SUB_2)
+
+ // ... and dropped from the cache
+ assertThat(underTest.mobileIconSubIdCache).containsExactly(2, model2.commonImpl)
+ }
+
+ companion object {
+ private val SUB_1 = SubscriptionModel(subscriptionId = 1, isOpportunistic = false)
+ private val SUB_2 = SubscriptionModel(subscriptionId = 2, isOpportunistic = false)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index 2f18ce3..4e15b4a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -16,9 +16,9 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -35,7 +35,7 @@
override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT)
- override val wifiActivity: StateFlow<WifiActivityModel> = _wifiActivity
+ override val wifiActivity: StateFlow<DataActivityModel> = _wifiActivity
fun setIsWifiEnabled(enabled: Boolean) {
_isWifiEnabled.value = enabled
@@ -49,7 +49,7 @@
_wifiNetwork.value = wifiNetworkModel
}
- fun setWifiActivity(activity: WifiActivityModel) {
+ fun setWifiActivity(activity: DataActivityModel) {
_wifiActivity.value = activity
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index 800f3c0..5d0d87b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -31,10 +31,10 @@
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -724,7 +724,7 @@
fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
underTest = createRepo(wifiManagerToUse = null)
- var latest: WifiActivityModel? = null
+ var latest: DataActivityModel? = null
val job = underTest
.wifiActivity
.onEach { latest = it }
@@ -737,7 +737,7 @@
@Test
fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) {
- var latest: WifiActivityModel? = null
+ var latest: DataActivityModel? = null
val job = underTest
.wifiActivity
.onEach { latest = it }
@@ -746,7 +746,7 @@
getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_NONE)
assertThat(latest).isEqualTo(
- WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+ DataActivityModel(hasActivityIn = false, hasActivityOut = false)
)
job.cancel()
@@ -754,7 +754,7 @@
@Test
fun wifiActivity_callbackGivesIn_activityFlowHasIn() = runBlocking(IMMEDIATE) {
- var latest: WifiActivityModel? = null
+ var latest: DataActivityModel? = null
val job = underTest
.wifiActivity
.onEach { latest = it }
@@ -763,7 +763,7 @@
getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_IN)
assertThat(latest).isEqualTo(
- WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
)
job.cancel()
@@ -771,7 +771,7 @@
@Test
fun wifiActivity_callbackGivesOut_activityFlowHasOut() = runBlocking(IMMEDIATE) {
- var latest: WifiActivityModel? = null
+ var latest: DataActivityModel? = null
val job = underTest
.wifiActivity
.onEach { latest = it }
@@ -780,7 +780,7 @@
getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_OUT)
assertThat(latest).isEqualTo(
- WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
)
job.cancel()
@@ -788,7 +788,7 @@
@Test
fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = runBlocking(IMMEDIATE) {
- var latest: WifiActivityModel? = null
+ var latest: DataActivityModel? = null
val job = underTest
.wifiActivity
.onEach { latest = it }
@@ -796,7 +796,7 @@
getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_INOUT)
- assertThat(latest).isEqualTo(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+ assertThat(latest).isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
job.cancel()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
new file mode 100644
index 0000000..b935442
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2022 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.wifi.data.repository
+
+import android.net.ConnectivityManager
+import android.net.wifi.WifiManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class WifiRepositorySwitcherTest : SysuiTestCase() {
+ private lateinit var underTest: WifiRepositorySwitcher
+ private lateinit var realImpl: WifiRepositoryImpl
+ private lateinit var demoImpl: DemoWifiRepository
+
+ @Mock private lateinit var demoModeController: DemoModeController
+ @Mock private lateinit var logger: ConnectivityPipelineLogger
+ @Mock private lateinit var tableLogger: TableLogBuffer
+ @Mock private lateinit var connectivityManager: ConnectivityManager
+ @Mock private lateinit var wifiManager: WifiManager
+ @Mock private lateinit var demoModeWifiDataSource: DemoModeWifiDataSource
+ private val demoModelFlow = MutableStateFlow<FakeWifiEventModel?>(null)
+
+ private val mainExecutor = FakeExecutor(FakeSystemClock())
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ // Never start in demo mode
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+
+ realImpl =
+ WifiRepositoryImpl(
+ fakeBroadcastDispatcher,
+ connectivityManager,
+ logger,
+ tableLogger,
+ mainExecutor,
+ testScope.backgroundScope,
+ wifiManager,
+ )
+
+ whenever(demoModeWifiDataSource.wifiEvents).thenReturn(demoModelFlow)
+
+ demoImpl =
+ DemoWifiRepository(
+ demoModeWifiDataSource,
+ testScope.backgroundScope,
+ )
+
+ underTest =
+ WifiRepositorySwitcher(
+ realImpl,
+ demoImpl,
+ demoModeController,
+ testScope.backgroundScope,
+ )
+ }
+
+ @Test
+ fun `switcher active repo - updates when demo mode changes`() =
+ testScope.runTest {
+ assertThat(underTest.activeRepo.value).isSameInstanceAs(realImpl)
+
+ var latest: WifiRepository? = null
+ val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
+
+ startDemoMode()
+
+ assertThat(latest).isSameInstanceAs(demoImpl)
+
+ finishDemoMode()
+
+ assertThat(latest).isSameInstanceAs(realImpl)
+
+ job.cancel()
+ }
+
+ private fun startDemoMode() {
+ whenever(demoModeController.isInDemoMode).thenReturn(true)
+ getDemoModeCallback().onDemoModeStarted()
+ }
+
+ private fun finishDemoMode() {
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+ getDemoModeCallback().onDemoModeFinished()
+ }
+
+ private fun getDemoModeCallback(): DemoMode {
+ val captor = kotlinArgumentCaptor<DemoMode>()
+ Mockito.verify(demoModeController).addCallback(captor.capture())
+ return captor.value
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index b38497a..2ecb17b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -20,10 +20,10 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -225,23 +225,23 @@
@Test
fun activity_matchesRepoWifiActivity() = runBlocking(IMMEDIATE) {
- var latest: WifiActivityModel? = null
+ var latest: DataActivityModel? = null
val job = underTest
.activity
.onEach { latest = it }
.launchIn(this)
- val activity1 = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+ val activity1 = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity1)
yield()
assertThat(latest).isEqualTo(activity1)
- val activity2 = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+ val activity2 = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
wifiRepository.setWifiActivity(activity2)
yield()
assertThat(latest).isEqualTo(activity2)
- val activity3 = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+ val activity3 = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
wifiRepository.setWifiActivity(activity3)
yield()
assertThat(latest).isEqualTo(activity3)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 7502020..4158434 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -27,13 +27,13 @@
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
@@ -146,7 +146,7 @@
@Test
fun activity_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -183,7 +183,7 @@
@Test
fun activity_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -209,7 +209,7 @@
.launchIn(this)
// WHEN we update the repo to have activity
- val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+ val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
@@ -225,7 +225,7 @@
@Test
fun activity_nullSsid_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null))
@@ -252,7 +252,7 @@
.launchIn(this)
// WHEN we update the repo to have activity
- val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+ val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
@@ -268,7 +268,7 @@
@Test
fun activity_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -293,7 +293,7 @@
.onEach { latestQs = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+ val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
@@ -308,7 +308,7 @@
@Test
fun activityIn_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -319,7 +319,7 @@
.onEach { latest = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+ val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
wifiRepository.setWifiActivity(activity)
yield()
@@ -330,7 +330,7 @@
@Test
fun activityIn_hasActivityInFalse_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -341,7 +341,7 @@
.onEach { latest = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+ val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
@@ -352,7 +352,7 @@
@Test
fun activityOut_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -363,7 +363,7 @@
.onEach { latest = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+ val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
@@ -374,7 +374,7 @@
@Test
fun activityOut_hasActivityOutFalse_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -385,7 +385,7 @@
.onEach { latest = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+ val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
wifiRepository.setWifiActivity(activity)
yield()
@@ -396,7 +396,7 @@
@Test
fun activityContainer_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -407,7 +407,7 @@
.onEach { latest = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+ val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = false)
wifiRepository.setWifiActivity(activity)
yield()
@@ -418,7 +418,7 @@
@Test
fun activityContainer_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -429,7 +429,7 @@
.onEach { latest = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+ val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
@@ -440,7 +440,7 @@
@Test
fun activityContainer_inAndOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -451,7 +451,7 @@
.onEach { latest = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
+ val activity = DataActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
@@ -462,7 +462,7 @@
@Test
fun activityContainer_inAndOutFalse_outputsFalse() = runBlocking(IMMEDIATE) {
- whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true)
+ whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
@@ -473,7 +473,7 @@
.onEach { latest = it }
.launchIn(this)
- val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+ val activity = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
wifiRepository.setWifiActivity(activity)
yield()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 2c47204..4b32ee2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -55,6 +55,7 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
+import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
@@ -414,7 +415,9 @@
mDependency,
TestableLooper.get(this));
ExpandableNotificationRow row = helper.createRow();
+ FrameLayout remoteInputViewParent = new FrameLayout(mContext);
RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+ remoteInputViewParent.addView(view);
bindController(view, row.getEntry());
// Start defocus animation
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
index 82153d5..99e2012 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
import com.android.systemui.util.concurrency.DelayableExecutor
@@ -66,6 +67,8 @@
@Mock
private lateinit var configurationController: ConfigurationController
@Mock
+ private lateinit var dumpManager: DumpManager
+ @Mock
private lateinit var windowManager: WindowManager
@Mock
private lateinit var powerManager: PowerManager
@@ -91,6 +94,7 @@
fakeExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
fakeWakeLockBuilder,
fakeClock,
@@ -989,6 +993,7 @@
@Main mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
+ dumpManager: DumpManager,
powerManager: PowerManager,
wakeLockBuilder: WakeLock.Builder,
systemClock: SystemClock,
@@ -999,6 +1004,7 @@
mainExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
R.layout.chipbar,
wakeLockBuilder,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 2e4d8e7..d3411c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -36,6 +36,7 @@
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Text
import com.android.systemui.common.shared.model.TintedIcon
+import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -66,6 +67,7 @@
@Mock private lateinit var logger: ChipbarLogger
@Mock private lateinit var accessibilityManager: AccessibilityManager
@Mock private lateinit var configurationController: ConfigurationController
+ @Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var powerManager: PowerManager
@Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var falsingManager: FalsingManager
@@ -100,6 +102,7 @@
fakeExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
falsingManager,
falsingCollector,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
index d5167b3..4ef4e6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -22,6 +22,7 @@
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -38,6 +39,7 @@
mainExecutor: DelayableExecutor,
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
+ dumpManager: DumpManager,
powerManager: PowerManager,
falsingManager: FalsingManager,
falsingCollector: FalsingCollector,
@@ -53,6 +55,7 @@
mainExecutor,
accessibilityManager,
configurationController,
+ dumpManager,
powerManager,
falsingManager,
falsingCollector,
@@ -61,7 +64,7 @@
wakeLockBuilder,
systemClock,
) {
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
onAnimationEnd.run()
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 5501949..39d2eca 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -46,12 +46,18 @@
private val _isKeyguardShowing = MutableStateFlow(false)
override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
+ private val _isKeyguardOccluded = MutableStateFlow(false)
+ override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded
+
private val _isDozing = MutableStateFlow(false)
override val isDozing: Flow<Boolean> = _isDozing
private val _isDreaming = MutableStateFlow(false)
override val isDreaming: Flow<Boolean> = _isDreaming
+ private val _isDreamingWithOverlay = MutableStateFlow(false)
+ override val isDreamingWithOverlay: Flow<Boolean> = _isDreamingWithOverlay
+
private val _dozeAmount = MutableStateFlow(0f)
override val linearDozeAmount: Flow<Float> = _dozeAmount
@@ -112,10 +118,18 @@
_isKeyguardShowing.value = isShowing
}
+ fun setKeyguardOccluded(isOccluded: Boolean) {
+ _isKeyguardOccluded.value = isOccluded
+ }
+
fun setDozing(isDozing: Boolean) {
_isDozing.value = isDozing
}
+ fun setDreamingWithOverlay(isDreaming: Boolean) {
+ _isDreamingWithOverlay.value = isDreaming
+ }
+
fun setDozeAmount(dozeAmount: Float) {
_dozeAmount.value = dozeAmount
}
@@ -144,6 +158,10 @@
_fingerprintSensorLocation.tryEmit(location)
}
+ fun setDozeTransitionModel(model: DozeTransitionModel) {
+ _dozeTransitionModel.value = model
+ }
+
override fun isUdfpsSupported(): Boolean {
return _isUdfpsSupported.value
}
diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto
index f26404c6..da4dfa9 100644
--- a/proto/src/windowmanager.proto
+++ b/proto/src/windowmanager.proto
@@ -68,4 +68,8 @@
LetterboxHorizontalReachability letterbox_position_for_horizontal_reachability = 1;
// Represents the current vertical position for the letterboxed activity
LetterboxVerticalReachability letterbox_position_for_vertical_reachability = 2;
+ // Represents the current horizontal position for the letterboxed activity in book mode
+ LetterboxHorizontalReachability letterbox_position_for_book_mode_reachability = 3;
+ // Represents the current vertical position for the letterboxed activity in tabletop mode
+ LetterboxVerticalReachability letterbox_position_for_tabletop_mode_reachability = 4;
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index aec60de..7bbc604 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -74,6 +74,7 @@
import android.view.WindowManagerGlobal;
import com.android.framework.protobuf.nano.MessageNano;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
@@ -389,6 +390,16 @@
return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
}
+ // When config_isWindowManagerCameraCompatTreatmentEnabled is true,
+ // DisplayRotationCompatPolicy in WindowManager force rotates fullscreen activities with
+ // fixed orientation to align them with the natural orientation of the device.
+ if (ctx.getResources().getBoolean(
+ R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) {
+ Slog.v(TAG, "Disable Rotate and Crop to avoid conflicts with"
+ + " WM force rotation treatment.");
+ return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
+ }
+
// External cameras do not need crop-rotate-scale.
if (lensFacing != CameraMetadata.LENS_FACING_FRONT
&& lensFacing != CameraMetadata.LENS_FACING_BACK) {
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 401d184..5bdfa70 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -46,6 +46,7 @@
import com.android.server.display.config.DisplayQuirks;
import com.android.server.display.config.HbmTiming;
import com.android.server.display.config.HighBrightnessMode;
+import com.android.server.display.config.IntegerArray;
import com.android.server.display.config.NitsMap;
import com.android.server.display.config.Point;
import com.android.server.display.config.RefreshRateConfigs;
@@ -213,6 +214,10 @@
* <type>android.sensor.light</type>
* <name>1234 Ambient Light Sensor</name>
* </lightSensor>
+ * <screenOffBrightnessSensor>
+ * <type>com.google.sensor.binned_brightness</type>
+ * <name>Binned Brightness 0 (wake-up)</name>
+ * </screenOffBrightnessSensor>
* <proxSensor>
* <type>android.sensor.proximity</type>
* <name>1234 Proximity Sensor</name>
@@ -368,6 +373,13 @@
* </brightnessThresholdPoints>
* </darkeningThresholds>
* </displayBrightnessChangeThresholdsIdle>
+ * <screenOffBrightnessSensorValueToLux>
+ * <item>-1</item>
+ * <item>0</item>
+ * <item>5</item>
+ * <item>80</item>
+ * <item>1500</item>
+ * </screenOffBrightnessSensorValueToLux>
* </displayConfiguration>
* }
* </pre>
@@ -428,6 +440,9 @@
// The details of the ambient light sensor associated with this display.
private final SensorData mAmbientLightSensor = new SensorData();
+ // The details of the doze brightness sensor associated with this display.
+ private final SensorData mScreenOffBrightnessSensor = new SensorData();
+
// The details of the proximity sensor associated with this display.
private final SensorData mProximitySensor = new SensorData();
@@ -523,6 +538,9 @@
private float[] mAmbientDarkeningLevelsIdle = DEFAULT_AMBIENT_THRESHOLD_LEVELS;
private float[] mAmbientDarkeningPercentagesIdle = DEFAULT_AMBIENT_DARKENING_THRESHOLDS;
+ // A mapping between screen off sensor values and lux values
+ private int[] mScreenOffBrightnessSensorValueToLux;
+
private Spline mBrightnessToBacklightSpline;
private Spline mBacklightToBrightnessSpline;
private Spline mBacklightToNitsSpline;
@@ -1198,6 +1216,10 @@
return mAmbientLightSensor;
}
+ SensorData getScreenOffBrightnessSensor() {
+ return mScreenOffBrightnessSensor;
+ }
+
SensorData getProximitySensor() {
return mProximitySensor;
}
@@ -1321,6 +1343,14 @@
return mHighAmbientBrightnessThresholds;
}
+ /**
+ * @return A mapping from screen off brightness sensor readings to lux values. This estimates
+ * the ambient lux when the screen is off to determine the initial brightness
+ */
+ public int[] getScreenOffBrightnessSensorValueToLux() {
+ return mScreenOffBrightnessSensorValueToLux;
+ }
+
@Override
public String toString() {
return "DisplayDeviceConfig{"
@@ -1399,6 +1429,7 @@
mScreenDarkeningPercentagesIdle)
+ "\n"
+ ", mAmbientLightSensor=" + mAmbientLightSensor
+ + ", mScreenOffBrightnessSensor=" + mScreenOffBrightnessSensor
+ ", mProximitySensor=" + mProximitySensor
+ ", mRefreshRateLimitations= " + Arrays.toString(mRefreshRateLimitations.toArray())
+ ", mDensityMapping= " + mDensityMapping
@@ -1421,6 +1452,9 @@
+ Arrays.toString(mHighDisplayBrightnessThresholds)
+ ", mHighAmbientBrightnessThresholds= "
+ Arrays.toString(mHighAmbientBrightnessThresholds)
+ + "\n"
+ + ", mScreenOffBrightnessSensorValueToLux=" + Arrays.toString(
+ mScreenOffBrightnessSensorValueToLux)
+ "}";
}
@@ -1474,11 +1508,13 @@
loadQuirks(config);
loadBrightnessRamps(config);
loadAmbientLightSensorFromDdc(config);
+ loadScreenOffBrightnessSensorFromDdc(config);
loadProxSensorFromDdc(config);
loadAmbientHorizonFromDdc(config);
loadBrightnessChangeThresholds(config);
loadAutoBrightnessConfigValues(config);
loadRefreshRateSetting(config);
+ loadScreenOffBrightnessSensorValueToLuxFromDdc(config);
} else {
Slog.w(TAG, "DisplayDeviceConfig file is null");
}
@@ -2175,6 +2211,14 @@
mProximitySensor.type = "";
}
+ private void loadScreenOffBrightnessSensorFromDdc(DisplayConfiguration config) {
+ final SensorDetails sensorDetails = config.getScreenOffBrightnessSensor();
+ if (sensorDetails != null) {
+ mScreenOffBrightnessSensor.type = sensorDetails.getType();
+ mScreenOffBrightnessSensor.name = sensorDetails.getName();
+ }
+ }
+
private void loadProxSensorFromDdc(DisplayConfiguration config) {
SensorDetails sensorDetails = config.getProxSensor();
if (sensorDetails != null) {
@@ -2587,6 +2631,19 @@
&& mDdcAutoBrightnessAvailable;
}
+ private void loadScreenOffBrightnessSensorValueToLuxFromDdc(DisplayConfiguration config) {
+ IntegerArray sensorValueToLux = config.getScreenOffBrightnessSensorValueToLux();
+ if (sensorValueToLux == null) {
+ return;
+ }
+
+ List<BigInteger> items = sensorValueToLux.getItem();
+ mScreenOffBrightnessSensorValueToLux = new int[items.size()];
+ for (int i = 0; i < items.size(); i++) {
+ mScreenOffBrightnessSensorValueToLux[i] = items.get(i).intValue();
+ }
+ }
+
static class SensorData {
public String type;
public String name;
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index c740b98..d791c06 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -414,7 +414,12 @@
@Nullable
private AutomaticBrightnessController mAutomaticBrightnessController;
+ // The controller for the sensor used to estimate ambient lux while the display is off.
+ @Nullable
+ private ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController;
+
private Sensor mLightSensor;
+ private Sensor mScreenOffBrightnessSensor;
// The mappers between ambient lux, display backlight values, and display brightness.
// We will switch between the idle mapper and active mapper in AutomaticBrightnessController.
@@ -1098,6 +1103,19 @@
mBrightnessEventRingBuffer =
new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX);
+
+ loadScreenOffBrightnessSensor();
+ int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux();
+ if (mScreenOffBrightnessSensor != null && sensorValueToLux != null) {
+ mScreenOffBrightnessSensorController = new ScreenOffBrightnessSensorController(
+ mSensorManager,
+ mScreenOffBrightnessSensor,
+ mHandler,
+ SystemClock::uptimeMillis,
+ sensorValueToLux,
+ mInteractiveModeBrightnessMapper
+ );
+ }
} else {
mUseSoftwareAutoBrightnessConfig = false;
}
@@ -1275,6 +1293,12 @@
}
assert(state != Display.STATE_UNKNOWN);
+ if (mScreenOffBrightnessSensorController != null) {
+ mScreenOffBrightnessSensorController.setLightSensorEnabled(mUseAutoBrightness
+ && (state == Display.STATE_OFF || (state == Display.STATE_DOZE
+ && !mAllowAutoBrightnessWhileDozingConfig)));
+ }
+
boolean skipRampBecauseOfProximityChangeToNegative = false;
// Apply the proximity sensor.
if (mProximitySensor != null) {
@@ -1454,6 +1478,9 @@
updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState;
mAppliedAutoBrightness = true;
mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC);
+ if (mScreenOffBrightnessSensorController != null) {
+ mScreenOffBrightnessSensorController.setLightSensorEnabled(false);
+ }
} else {
mAppliedAutoBrightness = false;
}
@@ -1481,6 +1508,19 @@
mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
}
+ // The ALS is not available yet - use the screen off sensor to determine the initial
+ // brightness
+ if (Float.isNaN(brightnessState) && autoBrightnessEnabled
+ && mScreenOffBrightnessSensorController != null) {
+ brightnessState = mScreenOffBrightnessSensorController.getAutomaticScreenBrightness();
+ if (isValidBrightnessValue(brightnessState)) {
+ brightnessState = clampScreenBrightness(brightnessState);
+ updateScreenBrightnessSetting = mCurrentScreenBrightnessSetting != brightnessState;
+ mBrightnessReasonTemp.setReason(
+ BrightnessReason.REASON_SCREEN_OFF_BRIGHTNESS_SENSOR);
+ }
+ }
+
// Apply manual brightness.
if (Float.isNaN(brightnessState)) {
brightnessState = clampScreenBrightness(mCurrentScreenBrightnessSetting);
@@ -2052,6 +2092,14 @@
fallbackType);
}
+ private void loadScreenOffBrightnessSensor() {
+ DisplayDeviceConfig.SensorData screenOffBrightnessSensor =
+ mDisplayDeviceConfig.getScreenOffBrightnessSensor();
+ mScreenOffBrightnessSensor = SensorUtils.findSensor(mSensorManager,
+ screenOffBrightnessSensor.type, screenOffBrightnessSensor.name,
+ SensorUtils.NO_FALLBACK);
+ }
+
private void loadProximitySensor() {
if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT) {
return;
@@ -3201,7 +3249,8 @@
static final int REASON_OVERRIDE = 7;
static final int REASON_TEMPORARY = 8;
static final int REASON_BOOST = 9;
- static final int REASON_MAX = REASON_BOOST;
+ static final int REASON_SCREEN_OFF_BRIGHTNESS_SENSOR = 10;
+ static final int REASON_MAX = REASON_SCREEN_OFF_BRIGHTNESS_SENSOR;
static final int MODIFIER_DIMMED = 0x1;
static final int MODIFIER_LOW_POWER = 0x2;
@@ -3311,6 +3360,7 @@
case REASON_OVERRIDE: return "override";
case REASON_TEMPORARY: return "temporary";
case REASON_BOOST: return "boost";
+ case REASON_SCREEN_OFF_BRIGHTNESS_SENSOR: return "screen_off_brightness_sensor";
default: return Integer.toString(reason);
}
}
diff --git a/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java
new file mode 100644
index 0000000..6f50dac
--- /dev/null
+++ b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import android.annotation.Nullable;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+/**
+ * Controls the light sensor when the screen is off. The sensor used here does not report lux values
+ * but an index that needs to be mapped to a lux value.
+ */
+public class ScreenOffBrightnessSensorController implements SensorEventListener {
+ private static final String TAG = "ScreenOffBrightnessSensorController";
+
+ private static final int SENSOR_INVALID_VALUE = -1;
+ private static final long SENSOR_VALUE_VALID_TIME_MILLIS = 1500;
+
+ private final Handler mHandler;
+ private final Clock mClock;
+ private final SensorManager mSensorManager;
+ private final Sensor mLightSensor;
+ private final int[] mSensorValueToLux;
+
+ private boolean mRegistered;
+ private int mLastSensorValue = SENSOR_INVALID_VALUE;
+ private long mSensorDisableTime = -1;
+
+ // The mapper to translate ambient lux to screen brightness in the range [0, 1.0].
+ @Nullable
+ private final BrightnessMappingStrategy mBrightnessMapper;
+
+ public ScreenOffBrightnessSensorController(
+ SensorManager sensorManager,
+ Sensor lightSensor,
+ Handler handler,
+ Clock clock,
+ int[] sensorValueToLux,
+ BrightnessMappingStrategy brightnessMapper) {
+ mSensorManager = sensorManager;
+ mLightSensor = lightSensor;
+ mHandler = handler;
+ mClock = clock;
+ mSensorValueToLux = sensorValueToLux;
+ mBrightnessMapper = brightnessMapper;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (mRegistered) {
+ mLastSensorValue = (int) event.values[0];
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ void setLightSensorEnabled(boolean enabled) {
+ if (enabled && !mRegistered) {
+ // Wait until we get an event from the sensor indicating ready.
+ mRegistered = mSensorManager.registerListener(this, mLightSensor,
+ SensorManager.SENSOR_DELAY_NORMAL, mHandler);
+ mLastSensorValue = SENSOR_INVALID_VALUE;
+ } else if (!enabled && mRegistered) {
+ mSensorManager.unregisterListener(this);
+ mRegistered = false;
+ mSensorDisableTime = mClock.uptimeMillis();
+ }
+ }
+
+ float getAutomaticScreenBrightness() {
+ if (mLastSensorValue < 0 || mLastSensorValue >= mSensorValueToLux.length
+ || (!mRegistered
+ && mClock.uptimeMillis() - mSensorDisableTime > SENSOR_VALUE_VALID_TIME_MILLIS)) {
+ return PowerManager.BRIGHTNESS_INVALID_FLOAT;
+ }
+
+ int lux = mSensorValueToLux[mLastSensorValue];
+ if (lux < 0) {
+ return PowerManager.BRIGHTNESS_INVALID_FLOAT;
+ }
+
+ return mBrightnessMapper.getBrightness(lux);
+ }
+
+ /** Dump current state */
+ public void dump(PrintWriter pw) {
+ pw.println("ScreenOffBrightnessSensorController:");
+ IndentingPrintWriter idpw = new IndentingPrintWriter(pw);
+ idpw.increaseIndent();
+ idpw.println("registered=" + mRegistered);
+ idpw.println("lastSensorValue=" + mLastSensorValue);
+ }
+
+ /** Functional interface for providing time. */
+ @VisibleForTesting
+ interface Clock {
+ /**
+ * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
+ */
+ long uptimeMillis();
+ }
+}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index fa6f4ee..890c891 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1470,15 +1470,9 @@
}
// Then make sure none of the activities have more than the max number of shortcuts.
- int total = 0;
for (int i = counts.size() - 1; i >= 0; i--) {
- int count = counts.valueAt(i);
- service.enforceMaxActivityShortcuts(count);
- total += count;
+ service.enforceMaxActivityShortcuts(counts.valueAt(i));
}
-
- // Finally make sure that the app doesn't have more than the max number of shortcuts.
- service.enforceMaxAppShortcuts(total);
}
/**
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 014a77b..0b20683 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -181,9 +181,6 @@
static final int DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY = 15;
@VisibleForTesting
- static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 60;
-
- @VisibleForTesting
static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
@VisibleForTesting
@@ -260,11 +257,6 @@
String KEY_MAX_SHORTCUTS = "max_shortcuts";
/**
- * Key name for the max dynamic shortcuts per app. (int)
- */
- String KEY_MAX_SHORTCUTS_PER_APP = "max_shortcuts_per_app";
-
- /**
* Key name for icon compression quality, 0-100.
*/
String KEY_ICON_QUALITY = "icon_quality";
@@ -337,14 +329,9 @@
new SparseArray<>();
/**
- * Max number of dynamic + manifest shortcuts that each activity can have at a time.
- */
- private int mMaxShortcutsPerActivity;
-
- /**
* Max number of dynamic + manifest shortcuts that each application can have at a time.
*/
- private int mMaxShortcutsPerApp;
+ private int mMaxShortcuts;
/**
* Max number of updating API calls that each application can make during the interval.
@@ -817,12 +804,9 @@
mMaxUpdatesPerInterval = Math.max(0, (int) parser.getLong(
ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL, DEFAULT_MAX_UPDATES_PER_INTERVAL));
- mMaxShortcutsPerActivity = Math.max(0, (int) parser.getLong(
+ mMaxShortcuts = Math.max(0, (int) parser.getLong(
ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY));
- mMaxShortcutsPerApp = Math.max(0, (int) parser.getLong(
- ConfigConstants.KEY_MAX_SHORTCUTS_PER_APP, DEFAULT_MAX_SHORTCUTS_PER_APP));
-
final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
? (int) parser.getLong(
ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
@@ -1762,33 +1746,16 @@
* {@link #getMaxActivityShortcuts()}.
*/
void enforceMaxActivityShortcuts(int numShortcuts) {
- if (numShortcuts > mMaxShortcutsPerActivity) {
+ if (numShortcuts > mMaxShortcuts) {
throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
}
}
/**
- * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
- * {@link #getMaxAppShortcuts()}.
- */
- void enforceMaxAppShortcuts(int numShortcuts) {
- if (numShortcuts > mMaxShortcutsPerApp) {
- throw new IllegalArgumentException("Max number of dynamic shortcuts per app exceeded");
- }
- }
-
- /**
* Return the max number of dynamic + manifest shortcuts for each launcher icon.
*/
int getMaxActivityShortcuts() {
- return mMaxShortcutsPerActivity;
- }
-
- /**
- * Return the max number of dynamic + manifest shortcuts for each launcher icon.
- */
- int getMaxAppShortcuts() {
- return mMaxShortcutsPerApp;
+ return mMaxShortcuts;
}
/**
@@ -2221,8 +2188,6 @@
ps.ensureNotImmutable(shortcut.getId(), /*ignoreInvisible=*/ true);
fillInDefaultActivity(Arrays.asList(shortcut));
- enforceMaxAppShortcuts(ps.getShortcutCount());
-
if (!shortcut.hasRank()) {
shortcut.setRank(0);
}
@@ -2610,7 +2575,7 @@
throws RemoteException {
verifyCaller(packageName, userId);
- return mMaxShortcutsPerActivity;
+ return mMaxShortcuts;
}
@Override
@@ -4758,7 +4723,7 @@
pw.print(" maxUpdatesPerInterval: ");
pw.println(mMaxUpdatesPerInterval);
pw.print(" maxShortcutsPerActivity: ");
- pw.println(mMaxShortcutsPerActivity);
+ pw.println(mMaxShortcuts);
pw.println();
mStatLogger.dump(pw, " ");
@@ -5245,7 +5210,7 @@
@VisibleForTesting
int getMaxShortcutsForTest() {
- return mMaxShortcutsPerActivity;
+ return mMaxShortcuts;
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index dbe049a..de0d1f8 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1049,13 +1049,6 @@
return;
}
- // Don't dream if the user isn't user zero.
- // TODO(b/261907079): Move this check to DreamManagerService#canStartDreamingInternal().
- if (ActivityManager.getCurrentUser() != UserHandle.USER_SYSTEM) {
- noDreamAction.run();
- return;
- }
-
final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal();
if (dreamManagerInternal == null || !dreamManagerInternal.canStartDreaming(isScreenOn)) {
noDreamAction.run();
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index da6e7e8..7252545 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -163,6 +163,15 @@
}
@Override
+ public void activityRefreshed(IBinder token) {
+ final long origId = Binder.clearCallingIdentity();
+ synchronized (mGlobalLock) {
+ ActivityRecord.activityRefreshedLocked(token);
+ }
+ Binder.restoreCallingIdentity(origId);
+ }
+
+ @Override
public void activityTopResumedStateLost() {
final long origId = Binder.clearCallingIdentity();
synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 418e1ed..50169b4 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -736,7 +736,6 @@
*/
private boolean mWillCloseOrEnterPip;
- @VisibleForTesting
final LetterboxUiController mLetterboxUiController;
/**
@@ -6111,6 +6110,19 @@
r.mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(r);
}
+ static void activityRefreshedLocked(IBinder token) {
+ final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+ ProtoLog.i(WM_DEBUG_STATES, "Refreshed activity: %s", r);
+ if (r == null) {
+ // In case the record on server side has been removed (e.g. destroy timeout)
+ // and the token could be null.
+ return;
+ }
+ if (r.mDisplayContent.mDisplayRotationCompatPolicy != null) {
+ r.mDisplayContent.mDisplayRotationCompatPolicy.onActivityRefreshed(r);
+ }
+ }
+
static void splashScreenAttachedLocked(IBinder token) {
final ActivityRecord r = ActivityRecord.forTokenLocked(token);
if (r == null) {
@@ -8149,7 +8161,7 @@
}
/**
- * Adjusts position of resolved bounds if they doesn't fill the parent using gravity
+ * Adjusts position of resolved bounds if they don't fill the parent using gravity
* requested in the config or via an ADB command. For more context see {@link
* LetterboxUiController#getHorizontalPositionMultiplier(Configuration)} and
* {@link LetterboxUiController#getVerticalPositionMultiplier(Configuration)}
@@ -8168,9 +8180,8 @@
int offsetX = 0;
if (parentBounds.width() != screenResolvedBounds.width()) {
if (screenResolvedBounds.width() <= parentAppBounds.width()) {
- float positionMultiplier =
- mLetterboxUiController.getHorizontalPositionMultiplier(
- newParentConfiguration);
+ float positionMultiplier = mLetterboxUiController.getHorizontalPositionMultiplier(
+ newParentConfiguration);
offsetX = (int) Math.ceil((parentAppBounds.width() - screenResolvedBounds.width())
* positionMultiplier);
}
@@ -8180,9 +8191,8 @@
int offsetY = 0;
if (parentBounds.height() != screenResolvedBounds.height()) {
if (screenResolvedBounds.height() <= parentAppBounds.height()) {
- float positionMultiplier =
- mLetterboxUiController.getVerticalPositionMultiplier(
- newParentConfiguration);
+ float positionMultiplier = mLetterboxUiController.getVerticalPositionMultiplier(
+ newParentConfiguration);
offsetY = (int) Math.ceil((parentAppBounds.height() - screenResolvedBounds.height())
* positionMultiplier);
}
@@ -9145,6 +9155,8 @@
} else {
scheduleConfigurationChanged(newMergedOverrideConfig);
}
+ notifyDisplayCompatPolicyAboutConfigurationChange(
+ mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
return true;
}
@@ -9213,11 +9225,24 @@
} else {
scheduleConfigurationChanged(newMergedOverrideConfig);
}
+ notifyDisplayCompatPolicyAboutConfigurationChange(
+ mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
+
stopFreezingScreenLocked(false);
return true;
}
+ private void notifyDisplayCompatPolicyAboutConfigurationChange(
+ Configuration newConfig, Configuration lastReportedConfig) {
+ if (mDisplayContent.mDisplayRotationCompatPolicy == null
+ || !shouldBeResumed(/* activeActivity */ null)) {
+ return;
+ }
+ mDisplayContent.mDisplayRotationCompatPolicy.onActivityConfigurationChanging(
+ this, newConfig, lastReportedConfig);
+ }
+
/** Get process configuration, or global config if the process is not set. */
private Configuration getProcessGlobalConfiguration() {
return app != null ? app.getConfiguration() : mAtmService.getGlobalConfiguration();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4c49986..36f86d1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -431,6 +431,7 @@
private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
private final DisplayPolicy mDisplayPolicy;
private final DisplayRotation mDisplayRotation;
+ @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
DisplayFrames mDisplayFrames;
private final RemoteCallbackList<ISystemGestureExclusionListener>
@@ -1125,7 +1126,7 @@
}
mDisplayPolicy = new DisplayPolicy(mWmService, this);
- mDisplayRotation = new DisplayRotation(mWmService, this);
+ mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address);
mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH,
newFoldState -> {
@@ -1158,6 +1159,13 @@
onDisplayChanged(this);
updateDisplayAreaOrganizers();
+ mDisplayRotationCompatPolicy =
+ // Not checking DeviceConfig value here to allow enabling via DeviceConfig
+ // without the need to restart the device.
+ mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ false)
+ ? new DisplayRotationCompatPolicy(this) : null;
+
mInputMonitor = new InputMonitor(mWmService, this);
mInsetsPolicy = new InsetsPolicy(mInsetsStateController, this);
mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp();
@@ -2704,6 +2712,14 @@
}
}
+ if (mDisplayRotationCompatPolicy != null) {
+ int compatOrientation = mDisplayRotationCompatPolicy.getOrientation();
+ if (compatOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+ mLastOrientationSource = null;
+ return compatOrientation;
+ }
+ }
+
final int orientation = super.getOrientation();
if (!handlesOrientationChangeFromDescendant(orientation)) {
@@ -3260,6 +3276,10 @@
// on the next traversal if it's removed from RootWindowContainer child list.
getPendingTransaction().apply();
mWmService.mWindowPlacerLocked.requestTraversal();
+
+ if (mDisplayRotationCompatPolicy != null) {
+ mDisplayRotationCompatPolicy.dispose();
+ }
}
/** Returns true if a removal action is still being deferred. */
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 185e06e..cf3a688 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.util.RotationUtils.deltaRotation;
@@ -55,9 +56,11 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings;
+import android.util.ArraySet;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
+import android.view.DisplayAddress;
import android.view.IWindowManager;
import android.view.Surface;
import android.window.TransitionRequestInfo;
@@ -75,6 +78,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayDeque;
+import java.util.Set;
/**
* Defines the mapping between orientation and rotation of a display.
@@ -211,15 +215,16 @@
private boolean mDemoHdmiRotationLock;
private boolean mDemoRotationLock;
- DisplayRotation(WindowManagerService service, DisplayContent displayContent) {
- this(service, displayContent, displayContent.getDisplayPolicy(),
+ DisplayRotation(WindowManagerService service, DisplayContent displayContent,
+ DisplayAddress displayAddress) {
+ this(service, displayContent, displayAddress, displayContent.getDisplayPolicy(),
service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock());
}
@VisibleForTesting
DisplayRotation(WindowManagerService service, DisplayContent displayContent,
- DisplayPolicy displayPolicy, DisplayWindowSettings displayWindowSettings,
- Context context, Object lock) {
+ DisplayAddress displayAddress, DisplayPolicy displayPolicy,
+ DisplayWindowSettings displayWindowSettings, Context context, Object lock) {
mService = service;
mDisplayContent = displayContent;
mDisplayPolicy = displayPolicy;
@@ -235,6 +240,8 @@
mDeskDockRotation = readRotation(R.integer.config_deskDockRotation);
mUndockedHdmiRotation = readRotation(R.integer.config_undockedHdmiRotation);
+ mRotation = readDefaultDisplayRotation(displayAddress);
+
if (isDefaultDisplay) {
final Handler uiHandler = UiThread.getHandler();
mOrientationListener = new OrientationListener(mContext, uiHandler);
@@ -248,6 +255,33 @@
}
}
+ // Change the default value to the value specified in the sysprop
+ // ro.bootanim.set_orientation_<display_id>. Four values are supported: ORIENTATION_0,
+ // ORIENTATION_90, ORIENTATION_180 and ORIENTATION_270.
+ // If the value isn't specified or is ORIENTATION_0, nothing will be changed.
+ // This is needed to support having default orientation different from the natural
+ // device orientation. For example, on tablets that may want to keep natural orientation
+ // portrait for applications compatibility but have landscape orientation as a default choice
+ // from the UX perspective.
+ @Surface.Rotation
+ private int readDefaultDisplayRotation(DisplayAddress displayAddress) {
+ if (!(displayAddress instanceof DisplayAddress.Physical)) {
+ return Surface.ROTATION_0;
+ }
+ final DisplayAddress.Physical physicalAddress = (DisplayAddress.Physical) displayAddress;
+ String syspropValue = SystemProperties.get(
+ "ro.bootanim.set_orientation_" + physicalAddress.getPhysicalDisplayId(),
+ "ORIENTATION_0");
+ if (syspropValue.equals("ORIENTATION_90")) {
+ return Surface.ROTATION_90;
+ } else if (syspropValue.equals("ORIENTATION_180")) {
+ return Surface.ROTATION_180;
+ } else if (syspropValue.equals("ORIENTATION_270")) {
+ return Surface.ROTATION_270;
+ }
+ return Surface.ROTATION_0;
+ }
+
private int readRotation(int resID) {
try {
final int rotation = mContext.getResources().getInteger(resID);
@@ -1521,6 +1555,15 @@
proto.end(token);
}
+ boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
+ if (mFoldController == null) return false;
+ return mFoldController.isDeviceInPosture(state, isTabletop);
+ }
+
+ boolean isDisplaySeparatingHinge() {
+ return mFoldController != null && mFoldController.isSeparatingHinge();
+ }
+
/**
* Called by the DeviceStateManager callback when the device state changes.
*/
@@ -1538,6 +1581,63 @@
private DeviceStateController.FoldState mFoldState =
DeviceStateController.FoldState.UNKNOWN;
private boolean mInHalfFoldTransition = false;
+ private final boolean mIsDisplayAlwaysSeparatingHinge;
+ private final Set<Integer> mTabletopRotations;
+
+ FoldController() {
+ mTabletopRotations = new ArraySet<>();
+ int[] tabletop_rotations = mContext.getResources().getIntArray(
+ R.array.config_deviceTabletopRotations);
+ if (tabletop_rotations != null) {
+ for (int angle : tabletop_rotations) {
+ switch (angle) {
+ case 0:
+ mTabletopRotations.add(Surface.ROTATION_0);
+ break;
+ case 90:
+ mTabletopRotations.add(Surface.ROTATION_90);
+ break;
+ case 180:
+ mTabletopRotations.add(Surface.ROTATION_180);
+ break;
+ case 270:
+ mTabletopRotations.add(Surface.ROTATION_270);
+ break;
+ default:
+ ProtoLog.e(WM_DEBUG_ORIENTATION,
+ "Invalid surface rotation angle in "
+ + "config_deviceTabletopRotations: %d",
+ angle);
+ }
+ }
+ } else {
+ ProtoLog.w(WM_DEBUG_ORIENTATION,
+ "config_deviceTabletopRotations is not defined. Half-fold "
+ + "letterboxing will work inconsistently.");
+ }
+ mIsDisplayAlwaysSeparatingHinge = mContext.getResources().getBoolean(
+ R.bool.config_isDisplayHingeAlwaysSeparating);
+ }
+
+ boolean isDeviceInPosture(DeviceStateController.FoldState state, boolean isTabletop) {
+ if (state != mFoldState) {
+ return false;
+ }
+ if (mFoldState == DeviceStateController.FoldState.HALF_FOLDED) {
+ return !(isTabletop ^ mTabletopRotations.contains(mRotation));
+ }
+ return true;
+ }
+
+ DeviceStateController.FoldState getFoldState() {
+ return mFoldState;
+ }
+
+ boolean isSeparatingHinge() {
+ return mFoldState == DeviceStateController.FoldState.HALF_FOLDED
+ || (mFoldState == DeviceStateController.FoldState.OPEN
+ && mIsDisplayAlwaysSeparatingHinge);
+ }
boolean overrideFrozenRotation() {
return mFoldState == DeviceStateController.FoldState.HALF_FOLDED;
@@ -1586,6 +1686,15 @@
mService.updateRotation(false /* alwaysSendConfiguration */,
false /* forceRelayout */);
}
+ // Alert the activity of possible new bounds.
+ final Task topFullscreenTask =
+ mDisplayContent.getTask(t -> t.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
+ if (topFullscreenTask != null) {
+ final ActivityRecord top = topFullscreenTask.topRunningActivity();
+ if (top != null) {
+ top.recomputeConfiguration();
+ }
+ }
}
}
@@ -1696,6 +1805,7 @@
final int mHalfFoldSavedRotation;
final boolean mInHalfFoldTransition;
final DeviceStateController.FoldState mFoldState;
+ @Nullable final String mDisplayRotationCompatPolicySummary;
Record(DisplayRotation dr, int fromRotation, int toRotation) {
mFromRotation = fromRotation;
@@ -1730,6 +1840,10 @@
mInHalfFoldTransition = false;
mFoldState = DeviceStateController.FoldState.UNKNOWN;
}
+ mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null
+ ? null
+ : dc.mDisplayRotationCompatPolicy
+ .getSummaryForDisplayRotationHistoryRecord();
}
void dump(String prefix, PrintWriter pw) {
@@ -1752,6 +1866,9 @@
+ " mInHalfFoldTransition=" + mInHalfFoldTransition
+ " mFoldState=" + mFoldState);
}
+ if (mDisplayRotationCompatPolicySummary != null) {
+ pw.println(prefix + mDisplayRotationCompatPolicySummary);
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
new file mode 100644
index 0000000..18c5c3b
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2022 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;
+
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.screenOrientationToString;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.view.Display.TYPE_INTERNAL;
+
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.RefreshCallbackItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Controls camera compatibility treatment that handles orientation mismatch between camera
+ * buffers and an app window for a particular display that can lead to camera issues like sideways
+ * or stretched viewfinder.
+ *
+ * <p>This includes force rotation of fixed orientation activities connected to the camera.
+ *
+ * <p>The treatment is enabled for internal displays that have {@code ignoreOrientationRequest}
+ * display setting enabled and when {@code
+ * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
+ */
+ // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path
+final class DisplayRotationCompatPolicy {
+
+ // Delay for updating display rotation after Camera connection is closed. Needed to avoid
+ // rotation flickering when an app is flipping between front and rear cameras or when size
+ // compat mode is restarted.
+ // TODO(b/263114289): Consider associating this delay with a specific activity so that if
+ // the new non-camera activity started on top of the camer one we can rotate faster.
+ private static final int CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS = 2000;
+ // Delay for updating display rotation after Camera connection is opened. This delay is
+ // selected to be long enough to avoid conflicts with transitions on the app's side.
+ // Using a delay < CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app
+ // is flipping between front and rear cameras (in case requested orientation changes at
+ // runtime at the same time) or when size compat mode is restarted.
+ private static final int CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS =
+ CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS / 2;
+ // Delay for ensuring that onActivityRefreshed is always called after an activity refresh. The
+ // client process may not always report the event back to the server, such as process is
+ // crashed or got killed.
+ private static final int REFRESH_CALLBACK_TIMEOUT_MS = 2000;
+
+ private final DisplayContent mDisplayContent;
+ private final WindowManagerService mWmService;
+ private final CameraManager mCameraManager;
+ private final Handler mHandler;
+
+ // Bi-directional map between package names and active camera IDs since we need to 1) get a
+ // camera id by a package name when determining rotation; 2) get a package name by a camera id
+ // when camera connection is closed and we need to clean up our records.
+ @GuardedBy("this")
+ private final CameraIdPackageNameBiMap mCameraIdPackageBiMap = new CameraIdPackageNameBiMap();
+ @GuardedBy("this")
+ private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();
+ @GuardedBy("this")
+ private final Set<String> mScheduledOrientationUpdateCameraIdSet = new ArraySet<>();
+
+ private final CameraManager.AvailabilityCallback mAvailabilityCallback =
+ new CameraManager.AvailabilityCallback() {
+ @Override
+ public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
+ notifyCameraOpened(cameraId, packageId);
+ }
+
+ @Override
+ public void onCameraClosed(@NonNull String cameraId) {
+ notifyCameraClosed(cameraId);
+ }
+ };
+
+ @ScreenOrientation
+ private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET;
+
+ DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) {
+ this(displayContent, displayContent.mWmService.mH);
+ }
+
+ @VisibleForTesting
+ DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent, Handler handler) {
+ // This constructor is called from DisplayContent constructor. Don't use any fields in
+ // DisplayContent here since they aren't guaranteed to be set.
+ mHandler = handler;
+ mDisplayContent = displayContent;
+ mWmService = displayContent.mWmService;
+ mCameraManager = mWmService.mContext.getSystemService(CameraManager.class);
+ mCameraManager.registerAvailabilityCallback(
+ mWmService.mContext.getMainExecutor(), mAvailabilityCallback);
+ }
+
+ void dispose() {
+ mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback);
+ }
+
+ /**
+ * Determines orientation for Camera compatibility.
+ *
+ * <p>The goal of this function is to compute a orientation which would align orientations of
+ * portrait app window and natural orientation of the device and set opposite to natural
+ * orientation for a landscape app window. This is one of the strongest assumptions that apps
+ * make when they implement camera previews. Since app and natural display orientations aren't
+ * guaranteed to match, the rotation can cause letterboxing.
+ *
+ * <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link
+ * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment.
+ */
+ @ScreenOrientation
+ int getOrientation() {
+ mLastReportedOrientation = getOrientationInternal();
+ return mLastReportedOrientation;
+ }
+
+ @ScreenOrientation
+ private synchronized int getOrientationInternal() {
+ if (!isTreatmentEnabledForDisplay()) {
+ return SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (!isTreatmentEnabledForActivity(topActivity)) {
+ return SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ boolean isPortraitActivity =
+ topActivity.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT;
+ boolean isNaturalDisplayOrientationPortrait =
+ mDisplayContent.getNaturalOrientation() == ORIENTATION_PORTRAIT;
+ // Rotate portrait-only activity in the natural orientation of the displays (and in the
+ // opposite to natural orientation for landscape-only) since many apps assume that those
+ // are aligned when they compute orientation of the preview.
+ // This means that even for a landscape-only activity and a device with landscape natural
+ // orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that
+ // natural orientation = portrait window = portait camera is the main wrong assumption
+ // that apps make when they implement camera previews so landscape windows need be
+ // rotated in the orientation oposite to the natural one even if it's portrait.
+ // TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions
+ // of the portrait and landscape orientation requests.
+ int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait)
+ || (!isPortraitActivity && !isNaturalDisplayOrientationPortrait)
+ ? SCREEN_ORIENTATION_PORTRAIT
+ : SCREEN_ORIENTATION_LANDSCAPE;
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is ignoring all orientation requests, camera is active "
+ + "and the top activity is eligible for force rotation, return %s,"
+ + "portrait activity: %b, is natural orientation portrait: %b.",
+ mDisplayContent.mDisplayId, screenOrientationToString(orientation),
+ isPortraitActivity, isNaturalDisplayOrientationPortrait);
+ return orientation;
+ }
+
+ /**
+ * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle.
+ * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
+ * camera preview and can lead to sideways or stretching issues persisting even after force
+ * rotation.
+ */
+ void onActivityConfigurationChanging(ActivityRecord activity, Configuration newConfig,
+ Configuration lastReportedConfig) {
+ if (!isTreatmentEnabledForDisplay()
+ || !mWmService.mLetterboxConfiguration.isCameraCompatRefreshEnabled()
+ || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) {
+ return;
+ }
+ boolean cycleThroughStop = mWmService.mLetterboxConfiguration
+ .isCameraCompatRefreshCycleThroughStopEnabled();
+ try {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
+ ProtoLog.v(WM_DEBUG_STATES,
+ "Refershing activity for camera compatibility treatment, "
+ + "activityRecord=%s", activity);
+ final ClientTransaction transaction = ClientTransaction.obtain(
+ activity.app.getThread(), activity.token);
+ transaction.addCallback(
+ RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
+ transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false));
+ activity.mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+ mHandler.postDelayed(
+ () -> onActivityRefreshed(activity),
+ REFRESH_CALLBACK_TIMEOUT_MS);
+ } catch (RemoteException e) {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
+ }
+ }
+
+ void onActivityRefreshed(@NonNull ActivityRecord activity) {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
+ }
+
+ String getSummaryForDisplayRotationHistoryRecord() {
+ String summaryIfEnabled = "";
+ if (isTreatmentEnabledForDisplay()) {
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ summaryIfEnabled =
+ " mLastReportedOrientation="
+ + screenOrientationToString(mLastReportedOrientation)
+ + " topActivity="
+ + (topActivity == null ? "null" : topActivity.shortComponentName)
+ + " isTreatmentEnabledForActivity="
+ + isTreatmentEnabledForActivity(topActivity)
+ + " CameraIdPackageNameBiMap="
+ + mCameraIdPackageBiMap.getSummaryForDisplayRotationHistoryRecord();
+ }
+ return "DisplayRotationCompatPolicy{"
+ + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay()
+ + summaryIfEnabled
+ + " }";
+ }
+
+ // Refreshing only when configuration changes after rotation.
+ private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig,
+ Configuration lastReportedConfig) {
+ return newConfig.windowConfiguration.getDisplayRotation()
+ != lastReportedConfig.windowConfiguration.getDisplayRotation()
+ && isTreatmentEnabledForActivity(activity);
+ }
+
+ /**
+ * Whether camera compat treatment is enabled for the display.
+ *
+ * <p>Conditions that need to be met:
+ * <ul>
+ * <li>{@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
+ * <li>Setting {@code ignoreOrientationRequest} is enabled for the display.
+ * <li>Associated {@link DisplayContent} is for internal display. See b/225928882
+ * that tracks supporting external displays in the future.
+ * </ul>
+ */
+ private boolean isTreatmentEnabledForDisplay() {
+ return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true)
+ && mDisplayContent.getIgnoreOrientationRequest()
+ // TODO(b/225928882): Support camera compat rotation for external displays
+ && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
+ }
+
+ /**
+ * Whether camera compat treatment is applicable for the given activity.
+ *
+ * <p>Conditions that need to be met:
+ * <ul>
+ * <li>{@link #isCameraActiveForPackage} is {@code true} for the activity.
+ * <li>The activity is in fullscreen
+ * <li>The activity has fixed orientation but not "locked" or "nosensor" one.
+ * </ul>
+ */
+ private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
+ return activity != null && !activity.inMultiWindowMode()
+ && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
+ // "locked" and "nosensor" values are often used by camera apps that can't
+ // handle dynamic changes so we shouldn't force rotate them.
+ && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR
+ && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED
+ && mCameraIdPackageBiMap.containsPackageName(activity.packageName);
+ }
+
+ private synchronized void notifyCameraOpened(
+ @NonNull String cameraId, @NonNull String packageName) {
+ // If an activity is restarting or camera is flipping, the camera connection can be
+ // quickly closed and reopened.
+ mScheduledToBeRemovedCameraIdSet.remove(cameraId);
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is open for package %s",
+ mDisplayContent.mDisplayId, cameraId, packageName);
+ // Some apps can’t handle configuration changes coming at the same time with Camera setup
+ // so delaying orientation update to accomadate for that.
+ mScheduledOrientationUpdateCameraIdSet.add(cameraId);
+ mHandler.postDelayed(
+ () -> delayedUpdateOrientationWithWmLock(cameraId, packageName),
+ CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS);
+ }
+
+ private void updateOrientationWithWmLock() {
+ synchronized (mWmService.mGlobalLock) {
+ mDisplayContent.updateOrientation();
+ }
+ }
+
+ private void delayedUpdateOrientationWithWmLock(
+ @NonNull String cameraId, @NonNull String packageName) {
+ synchronized (this) {
+ if (!mScheduledOrientationUpdateCameraIdSet.remove(cameraId)) {
+ // Orientation update has happened already or was cancelled because
+ // camera was closed.
+ return;
+ }
+ mCameraIdPackageBiMap.put(packageName, cameraId);
+ }
+ updateOrientationWithWmLock();
+ }
+
+ private synchronized void notifyCameraClosed(@NonNull String cameraId) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
+ mDisplayContent.mDisplayId, cameraId);
+ mScheduledToBeRemovedCameraIdSet.add(cameraId);
+ // No need to update orientation for this camera if it's already closed.
+ mScheduledOrientationUpdateCameraIdSet.remove(cameraId);
+ scheduleRemoveCameraId(cameraId);
+ }
+
+ // Delay is needed to avoid rotation flickering when an app is flipping between front and
+ // rear cameras, when size compat mode is restarted or activity is being refreshed.
+ private void scheduleRemoveCameraId(@NonNull String cameraId) {
+ mHandler.postDelayed(
+ () -> removeCameraId(cameraId),
+ CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS);
+ }
+
+ private void removeCameraId(String cameraId) {
+ synchronized (this) {
+ if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) {
+ // Already reconnected to this camera, no need to clean up.
+ return;
+ }
+ if (isActivityForCameraIdRefreshing(cameraId)) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed but activity is"
+ + " still refreshing. Rescheduling an update.",
+ mDisplayContent.mDisplayId, cameraId);
+ mScheduledToBeRemovedCameraIdSet.add(cameraId);
+ scheduleRemoveCameraId(cameraId);
+ return;
+ }
+ mCameraIdPackageBiMap.removeCameraId(cameraId);
+ }
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed, updating rotation.",
+ mDisplayContent.mDisplayId, cameraId);
+ updateOrientationWithWmLock();
+ }
+
+ private boolean isActivityForCameraIdRefreshing(String cameraId) {
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (!isTreatmentEnabledForActivity(topActivity)) {
+ return false;
+ }
+ String activeCameraId = mCameraIdPackageBiMap.getCameraId(topActivity.packageName);
+ if (activeCameraId == null || activeCameraId != cameraId) {
+ return false;
+ }
+ return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested();
+ }
+
+ private static class CameraIdPackageNameBiMap {
+
+ private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>();
+ private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>();
+
+ void put(String packageName, String cameraId) {
+ // Always using the last connected camera ID for the package even for the concurrent
+ // camera use case since we can't guess which camera is more important anyway.
+ removePackageName(packageName);
+ removeCameraId(cameraId);
+ mPackageToCameraIdMap.put(packageName, cameraId);
+ mCameraIdToPackageMap.put(cameraId, packageName);
+ }
+
+ boolean containsPackageName(String packageName) {
+ return mPackageToCameraIdMap.containsKey(packageName);
+ }
+
+ @Nullable
+ String getCameraId(String packageName) {
+ return mPackageToCameraIdMap.get(packageName);
+ }
+
+ void removeCameraId(String cameraId) {
+ String packageName = mCameraIdToPackageMap.get(cameraId);
+ if (packageName == null) {
+ return;
+ }
+ mPackageToCameraIdMap.remove(packageName, cameraId);
+ mCameraIdToPackageMap.remove(cameraId, packageName);
+ }
+
+ String getSummaryForDisplayRotationHistoryRecord() {
+ return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }";
+ }
+
+ private void removePackageName(String packageName) {
+ String cameraId = mPackageToCameraIdMap.get(packageName);
+ if (cameraId == null) {
+ return;
+ }
+ mPackageToCameraIdMap.remove(packageName, cameraId);
+ mCameraIdToPackageMap.remove(cameraId, packageName);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 127a7bf..a7bf595f 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -16,12 +16,16 @@
package com.android.server.wm;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Color;
import android.provider.DeviceConfig;
+import android.util.Slog;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
@@ -33,6 +37,8 @@
/** Reads letterbox configs from resources and controls their overrides at runtime. */
final class LetterboxConfiguration {
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxConfiguration" : TAG_ATM;
+
/**
* Override of aspect ratio for fixed orientation letterboxing that is set via ADB with
* set-fixed-orientation-letterbox-aspect-ratio or via {@link
@@ -144,6 +150,14 @@
// side of the screen and 1.0 to the bottom side.
private float mLetterboxVerticalPositionMultiplier;
+ // Horizontal position of a center of the letterboxed app window when the device is half-folded.
+ // 0 corresponds to the left side of the screen and 1.0 to the right side.
+ private float mLetterboxBookModePositionMultiplier;
+
+ // Vertical position of a center of the letterboxed app window when the device is half-folded.
+ // 0 corresponds to the top side of the screen and 1.0 to the bottom side.
+ private float mLetterboxTabletopModePositionMultiplier;
+
// Default horizontal position the letterboxed app window when horizontal reachability is
// enabled and an app is fullscreen in landscape device orientation.
// It is used as a starting point for mLetterboxPositionForHorizontalReachability.
@@ -177,10 +191,31 @@
// Allows to enable letterboxing strategy for translucent activities ignoring flags.
private boolean mTranslucentLetterboxingOverrideEnabled;
+ // Whether camera compatibility treatment is enabled.
+ // See DisplayRotationCompatPolicy for context.
+ private final boolean mIsCameraCompatTreatmentEnabled;
+
+ // Whether activity "refresh" in camera compatibility treatment is enabled.
+ // See RefreshCallbackItem for context.
+ private boolean mIsCameraCompatTreatmentRefreshEnabled = true;
+
+ // Whether activity "refresh" in camera compatibility treatment should happen using the
+ // "stopped -> resumed" cycle rather than "paused -> resumed" cycle. Using "stop -> resumed"
+ // cycle by default due to higher success rate confirmed with app compatibility testing.
+ // See RefreshCallbackItem for context.
+ private boolean mIsCameraCompatRefreshCycleThroughStopEnabled = true;
+
LetterboxConfiguration(Context systemUiContext) {
this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
- () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext),
- () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext)));
+ () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
+ /* forBookMode */ false),
+ () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext,
+ /* forTabletopMode */ false),
+ () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
+ /* forBookMode */ true),
+ () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext,
+ /* forTabletopMode */ true)
+ ));
}
@VisibleForTesting
@@ -200,14 +235,18 @@
R.dimen.config_letterboxHorizontalPositionMultiplier);
mLetterboxVerticalPositionMultiplier = mContext.getResources().getFloat(
R.dimen.config_letterboxVerticalPositionMultiplier);
+ mLetterboxBookModePositionMultiplier = mContext.getResources().getFloat(
+ R.dimen.config_letterboxBookModePositionMultiplier);
+ mLetterboxTabletopModePositionMultiplier = mContext.getResources().getFloat(
+ R.dimen.config_letterboxTabletopModePositionMultiplier);
mIsHorizontalReachabilityEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsHorizontalReachabilityEnabled);
mIsVerticalReachabilityEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsVerticalReachabilityEnabled);
mDefaultPositionForHorizontalReachability =
- readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
+ readLetterboxHorizontalReachabilityPositionFromConfig(mContext, false);
mDefaultPositionForVerticalReachability =
- readLetterboxVerticalReachabilityPositionFromConfig(mContext);
+ readLetterboxVerticalReachabilityPositionFromConfig(mContext, false);
mIsEducationEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsEducationEnabled);
setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat(
@@ -216,6 +255,8 @@
R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsEnabledForTranslucentActivities);
+ mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
+ R.bool.config_isWindowManagerCameraCompatTreatmentEnabled);
mLetterboxConfigurationPersister = letterboxConfigurationPersister;
mLetterboxConfigurationPersister.start();
}
@@ -460,11 +501,30 @@
* or via an ADB command. 0 corresponds to the left side of the screen and 1 to the
* right side.
*/
- float getLetterboxHorizontalPositionMultiplier() {
- return (mLetterboxHorizontalPositionMultiplier < 0.0f
- || mLetterboxHorizontalPositionMultiplier > 1.0f)
- // Default to central position if invalid value is provided.
- ? 0.5f : mLetterboxHorizontalPositionMultiplier;
+ float getLetterboxHorizontalPositionMultiplier(boolean isInBookMode) {
+ if (isInBookMode) {
+ if (mLetterboxBookModePositionMultiplier < 0.0f
+ || mLetterboxBookModePositionMultiplier > 1.0f) {
+ Slog.w(TAG,
+ "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=true): "
+ + mLetterboxBookModePositionMultiplier);
+ // Default to left position if invalid value is provided.
+ return 0.0f;
+ } else {
+ return mLetterboxBookModePositionMultiplier;
+ }
+ } else {
+ if (mLetterboxHorizontalPositionMultiplier < 0.0f
+ || mLetterboxHorizontalPositionMultiplier > 1.0f) {
+ Slog.w(TAG,
+ "mLetterboxBookModePositionMultiplier out of bounds (isInBookMode=false):"
+ + mLetterboxBookModePositionMultiplier);
+ // Default to central position if invalid value is provided.
+ return 0.5f;
+ } else {
+ return mLetterboxHorizontalPositionMultiplier;
+ }
+ }
}
/*
@@ -473,11 +533,18 @@
* or via an ADB command. 0 corresponds to the top side of the screen and 1 to the
* bottom side.
*/
- float getLetterboxVerticalPositionMultiplier() {
- return (mLetterboxVerticalPositionMultiplier < 0.0f
- || mLetterboxVerticalPositionMultiplier > 1.0f)
- // Default to central position if invalid value is provided.
- ? 0.5f : mLetterboxVerticalPositionMultiplier;
+ float getLetterboxVerticalPositionMultiplier(boolean isInTabletopMode) {
+ if (isInTabletopMode) {
+ return (mLetterboxTabletopModePositionMultiplier < 0.0f
+ || mLetterboxTabletopModePositionMultiplier > 1.0f)
+ // Default to top position if invalid value is provided.
+ ? 0.0f : mLetterboxTabletopModePositionMultiplier;
+ } else {
+ return (mLetterboxVerticalPositionMultiplier < 0.0f
+ || mLetterboxVerticalPositionMultiplier > 1.0f)
+ // Default to central position if invalid value is provided.
+ ? 0.5f : mLetterboxVerticalPositionMultiplier;
+ }
}
/**
@@ -618,7 +685,8 @@
*/
void resetDefaultPositionForHorizontalReachability() {
mDefaultPositionForHorizontalReachability =
- readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
+ readLetterboxHorizontalReachabilityPositionFromConfig(mContext,
+ false /* forBookMode */);
}
/**
@@ -627,27 +695,34 @@
*/
void resetDefaultPositionForVerticalReachability() {
mDefaultPositionForVerticalReachability =
- readLetterboxVerticalReachabilityPositionFromConfig(mContext);
+ readLetterboxVerticalReachabilityPositionFromConfig(mContext,
+ false /* forTabletopMode */);
}
@LetterboxHorizontalReachabilityPosition
- private static int readLetterboxHorizontalReachabilityPositionFromConfig(Context context) {
+ private static int readLetterboxHorizontalReachabilityPositionFromConfig(Context context,
+ boolean forBookMode) {
int position = context.getResources().getInteger(
- R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+ forBookMode
+ ? R.integer.config_letterboxDefaultPositionForBookModeReachability
+ : R.integer.config_letterboxDefaultPositionForHorizontalReachability);
return position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT
- || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
- || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT
+ || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+ || position == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT
? position : LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
}
@LetterboxVerticalReachabilityPosition
- private static int readLetterboxVerticalReachabilityPositionFromConfig(Context context) {
+ private static int readLetterboxVerticalReachabilityPositionFromConfig(Context context,
+ boolean forTabletopMode) {
int position = context.getResources().getInteger(
- R.integer.config_letterboxDefaultPositionForVerticalReachability);
+ forTabletopMode
+ ? R.integer.config_letterboxDefaultPositionForTabletopModeReachability
+ : R.integer.config_letterboxDefaultPositionForVerticalReachability);
return position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP
|| position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
|| position == LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM
- ? position : LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+ ? position : LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
}
/*
@@ -656,9 +731,10 @@
*
* <p>The position multiplier is changed after each double tap in the letterbox area.
*/
- float getHorizontalMultiplierForReachability() {
+ float getHorizontalMultiplierForReachability(boolean isDeviceInBookMode) {
final int letterboxPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ isDeviceInBookMode);
switch (letterboxPositionForHorizontalReachability) {
case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
return 0.0f;
@@ -679,9 +755,10 @@
*
* <p>The position multiplier is changed after each double tap in the letterbox area.
*/
- float getVerticalMultiplierForReachability() {
+ float getVerticalMultiplierForReachability(boolean isDeviceInTabletopMode) {
final int letterboxPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+ isDeviceInTabletopMode);
switch (letterboxPositionForVerticalReachability) {
case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
return 0.0f;
@@ -701,8 +778,9 @@
* enabled.
*/
@LetterboxHorizontalReachabilityPosition
- int getLetterboxPositionForHorizontalReachability() {
- return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ int getLetterboxPositionForHorizontalReachability(boolean isInFullScreenBookMode) {
+ return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ isInFullScreenBookMode);
}
/*
@@ -710,8 +788,9 @@
* enabled.
*/
@LetterboxVerticalReachabilityPosition
- int getLetterboxPositionForVerticalReachability() {
- return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ int getLetterboxPositionForVerticalReachability(boolean isInFullScreenTabletopMode) {
+ return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+ isInFullScreenTabletopMode);
}
/** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */
@@ -750,34 +829,41 @@
* Changes letterbox position for horizontal reachability to the next available one on the
* right side.
*/
- void movePositionForHorizontalReachabilityToNextRightStop() {
- updatePositionForHorizontalReachability(prev -> Math.min(
- prev + 1, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
+ void movePositionForHorizontalReachabilityToNextRightStop(boolean isDeviceInBookMode) {
+ updatePositionForHorizontalReachability(isDeviceInBookMode, prev -> Math.min(
+ prev + (isDeviceInBookMode ? 2 : 1), // Move 2 stops in book mode to avoid center.
+ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
}
/**
* Changes letterbox position for horizontal reachability to the next available one on the left
* side.
*/
- void movePositionForHorizontalReachabilityToNextLeftStop() {
- updatePositionForHorizontalReachability(prev -> Math.max(prev - 1, 0));
+ void movePositionForHorizontalReachabilityToNextLeftStop(boolean isDeviceInBookMode) {
+ updatePositionForHorizontalReachability(isDeviceInBookMode, prev -> Math.max(
+ prev - (isDeviceInBookMode ? 2 : 1), 0)); // Move 2 stops in book mode to avoid
+ // center.
}
/**
* Changes letterbox position for vertical reachability to the next available one on the bottom
* side.
*/
- void movePositionForVerticalReachabilityToNextBottomStop() {
- updatePositionForVerticalReachability(prev -> Math.min(
- prev + 1, LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
+ void movePositionForVerticalReachabilityToNextBottomStop(boolean isDeviceInTabletopMode) {
+ updatePositionForVerticalReachability(isDeviceInTabletopMode, prev -> Math.min(
+ prev + (isDeviceInTabletopMode ? 2 : 1), // Move 2 stops in tabletop mode to avoid
+ // center.
+ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
}
/**
* Changes letterbox position for vertical reachability to the next available one on the top
* side.
*/
- void movePositionForVerticalReachabilityToNextTopStop() {
- updatePositionForVerticalReachability(prev -> Math.max(prev - 1, 0));
+ void movePositionForVerticalReachabilityToNextTopStop(boolean isDeviceInTabletopMode) {
+ updatePositionForVerticalReachability(isDeviceInTabletopMode, prev -> Math.max(
+ prev - (isDeviceInTabletopMode ? 2 : 1), 0)); // Move 2 stops in tabletop mode to
+ // avoid center.
}
/**
@@ -854,30 +940,88 @@
}
/** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */
- private void updatePositionForHorizontalReachability(
+ private void updatePositionForHorizontalReachability(boolean isDeviceInBookMode,
Function<Integer, Integer> newHorizonalPositionFun) {
final int letterboxPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ isDeviceInBookMode);
final int nextHorizontalPosition = newHorizonalPositionFun.apply(
letterboxPositionForHorizontalReachability);
mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
- nextHorizontalPosition);
+ isDeviceInBookMode, nextHorizontalPosition);
}
/** Calculates a new letterboxPositionForVerticalReachability value and updates the store */
- private void updatePositionForVerticalReachability(
+ private void updatePositionForVerticalReachability(boolean isDeviceInTabletopMode,
Function<Integer, Integer> newVerticalPositionFun) {
final int letterboxPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(
+ isDeviceInTabletopMode);
final int nextVerticalPosition = newVerticalPositionFun.apply(
letterboxPositionForVerticalReachability);
mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
- nextVerticalPosition);
+ isDeviceInTabletopMode, nextVerticalPosition);
}
- // TODO(b/262378106): Cache runtime flag and implement DeviceConfig.OnPropertiesChangedListener
+ // TODO(b/262378106): Cache a runtime flag and implement
+ // DeviceConfig.OnPropertiesChangedListener
static boolean isTranslucentLetterboxingAllowed() {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
"enable_translucent_activity_letterbox", false);
}
+
+ /** Whether camera compatibility treatment is enabled. */
+ boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
+ return mIsCameraCompatTreatmentEnabled
+ && (!checkDeviceConfig || isCameraCompatTreatmentAllowed());
+ }
+
+ // TODO(b/262977416): Cache a runtime flag and implement
+ // DeviceConfig.OnPropertiesChangedListener
+ private static boolean isCameraCompatTreatmentAllowed() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ "enable_camera_compat_treatment", false);
+ }
+
+ /** Whether camera compatibility refresh is enabled. */
+ boolean isCameraCompatRefreshEnabled() {
+ return mIsCameraCompatTreatmentRefreshEnabled;
+ }
+
+ /** Overrides whether camera compatibility treatment is enabled. */
+ void setCameraCompatRefreshEnabled(boolean enabled) {
+ mIsCameraCompatTreatmentRefreshEnabled = enabled;
+ }
+
+ /**
+ * Resets whether camera compatibility treatment is enabled to {@code true}.
+ */
+ void resetCameraCompatRefreshEnabled() {
+ mIsCameraCompatTreatmentRefreshEnabled = true;
+ }
+
+ /**
+ * Whether activity "refresh" in camera compatibility treatment should happen using the
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle.
+ */
+ boolean isCameraCompatRefreshCycleThroughStopEnabled() {
+ return mIsCameraCompatRefreshCycleThroughStopEnabled;
+ }
+
+ /**
+ * Overrides whether activity "refresh" in camera compatibility treatment should happen using
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle.
+ */
+ void setCameraCompatRefreshCycleThroughStopEnabled(boolean enabled) {
+ mIsCameraCompatRefreshCycleThroughStopEnabled = enabled;
+ }
+
+ /**
+ * Resets whether activity "refresh" in camera compatibility treatment should happen using
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle to {@code true}.
+ */
+ void resetCameraCompatRefreshCycleThroughStopEnabled() {
+ mIsCameraCompatRefreshCycleThroughStopEnabled = true;
+ }
+
}
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
index 70639b1..4a99db5 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
@@ -55,6 +55,8 @@
private final Context mContext;
private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier;
private final Supplier<Integer> mDefaultVerticalReachabilitySupplier;
+ private final Supplier<Integer> mDefaultBookModeReachabilitySupplier;
+ private final Supplier<Integer> mDefaultTabletopModeReachabilitySupplier;
// Horizontal position of a center of the letterboxed app window which is global to prevent
// "jumps" when switching between letterboxed apps. It's updated to reposition the app window
@@ -64,6 +66,11 @@
@LetterboxHorizontalReachabilityPosition
private volatile int mLetterboxPositionForHorizontalReachability;
+ // The same as mLetterboxPositionForHorizontalReachability but used when the device is
+ // half-folded.
+ @LetterboxHorizontalReachabilityPosition
+ private volatile int mLetterboxPositionForBookModeReachability;
+
// Vertical position of a center of the letterboxed app window which is global to prevent
// "jumps" when switching between letterboxed apps. It's updated to reposition the app window
// in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
@@ -72,6 +79,11 @@
@LetterboxVerticalReachabilityPosition
private volatile int mLetterboxPositionForVerticalReachability;
+ // The same as mLetterboxPositionForVerticalReachability but used when the device is
+ // half-folded.
+ @LetterboxVerticalReachabilityPosition
+ private volatile int mLetterboxPositionForTabletopModeReachability;
+
@NonNull
private final AtomicFile mConfigurationFile;
@@ -83,9 +95,13 @@
LetterboxConfigurationPersister(Context systemUiContext,
Supplier<Integer> defaultHorizontalReachabilitySupplier,
- Supplier<Integer> defaultVerticalReachabilitySupplier) {
+ Supplier<Integer> defaultVerticalReachabilitySupplier,
+ Supplier<Integer> defaultBookModeReachabilitySupplier,
+ Supplier<Integer> defaultTabletopModeReachabilitySupplier) {
this(systemUiContext, defaultHorizontalReachabilitySupplier,
defaultVerticalReachabilitySupplier,
+ defaultBookModeReachabilitySupplier,
+ defaultTabletopModeReachabilitySupplier,
Environment.getDataSystemDirectory(), new PersisterQueue(),
/* completionCallback */ null);
}
@@ -93,11 +109,18 @@
@VisibleForTesting
LetterboxConfigurationPersister(Context systemUiContext,
Supplier<Integer> defaultHorizontalReachabilitySupplier,
- Supplier<Integer> defaultVerticalReachabilitySupplier, File configFolder,
+ Supplier<Integer> defaultVerticalReachabilitySupplier,
+ Supplier<Integer> defaultBookModeReachabilitySupplier,
+ Supplier<Integer> defaultTabletopModeReachabilitySupplier,
+ File configFolder,
PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) {
mContext = systemUiContext.createDeviceProtectedStorageContext();
mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier;
mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier;
+ mDefaultBookModeReachabilitySupplier =
+ defaultBookModeReachabilitySupplier;
+ mDefaultTabletopModeReachabilitySupplier =
+ defaultTabletopModeReachabilitySupplier;
mCompletionCallback = completionCallback;
final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME);
mConfigurationFile = new AtomicFile(prefFiles);
@@ -117,8 +140,12 @@
* enabled.
*/
@LetterboxHorizontalReachabilityPosition
- int getLetterboxPositionForHorizontalReachability() {
- return mLetterboxPositionForHorizontalReachability;
+ int getLetterboxPositionForHorizontalReachability(boolean forBookMode) {
+ if (forBookMode) {
+ return mLetterboxPositionForBookModeReachability;
+ } else {
+ return mLetterboxPositionForHorizontalReachability;
+ }
}
/*
@@ -126,31 +153,55 @@
* enabled.
*/
@LetterboxVerticalReachabilityPosition
- int getLetterboxPositionForVerticalReachability() {
- return mLetterboxPositionForVerticalReachability;
- }
-
- /**
- * Updates letterboxPositionForVerticalReachability if different from the current value
- */
- void setLetterboxPositionForHorizontalReachability(
- int letterboxPositionForHorizontalReachability) {
- if (mLetterboxPositionForHorizontalReachability
- != letterboxPositionForHorizontalReachability) {
- mLetterboxPositionForHorizontalReachability =
- letterboxPositionForHorizontalReachability;
- updateConfiguration();
+ int getLetterboxPositionForVerticalReachability(boolean forTabletopMode) {
+ if (forTabletopMode) {
+ return mLetterboxPositionForTabletopModeReachability;
+ } else {
+ return mLetterboxPositionForVerticalReachability;
}
}
/**
* Updates letterboxPositionForVerticalReachability if different from the current value
*/
- void setLetterboxPositionForVerticalReachability(
+ void setLetterboxPositionForHorizontalReachability(boolean forBookMode,
+ int letterboxPositionForHorizontalReachability) {
+ if (forBookMode) {
+ if (mLetterboxPositionForBookModeReachability
+ != letterboxPositionForHorizontalReachability) {
+ mLetterboxPositionForBookModeReachability =
+ letterboxPositionForHorizontalReachability;
+ updateConfiguration();
+ }
+ } else {
+ if (mLetterboxPositionForHorizontalReachability
+ != letterboxPositionForHorizontalReachability) {
+ mLetterboxPositionForHorizontalReachability =
+ letterboxPositionForHorizontalReachability;
+ updateConfiguration();
+ }
+ }
+ }
+
+ /**
+ * Updates letterboxPositionForVerticalReachability if different from the current value
+ */
+ void setLetterboxPositionForVerticalReachability(boolean forTabletopMode,
int letterboxPositionForVerticalReachability) {
- if (mLetterboxPositionForVerticalReachability != letterboxPositionForVerticalReachability) {
- mLetterboxPositionForVerticalReachability = letterboxPositionForVerticalReachability;
- updateConfiguration();
+ if (forTabletopMode) {
+ if (mLetterboxPositionForTabletopModeReachability
+ != letterboxPositionForVerticalReachability) {
+ mLetterboxPositionForTabletopModeReachability =
+ letterboxPositionForVerticalReachability;
+ updateConfiguration();
+ }
+ } else {
+ if (mLetterboxPositionForVerticalReachability
+ != letterboxPositionForVerticalReachability) {
+ mLetterboxPositionForVerticalReachability =
+ letterboxPositionForVerticalReachability;
+ updateConfiguration();
+ }
}
}
@@ -158,6 +209,10 @@
void useDefaultValue() {
mLetterboxPositionForHorizontalReachability = mDefaultHorizontalReachabilitySupplier.get();
mLetterboxPositionForVerticalReachability = mDefaultVerticalReachabilitySupplier.get();
+ mLetterboxPositionForBookModeReachability =
+ mDefaultBookModeReachabilitySupplier.get();
+ mLetterboxPositionForTabletopModeReachability =
+ mDefaultTabletopModeReachabilitySupplier.get();
}
private void readCurrentConfiguration() {
@@ -171,6 +226,10 @@
letterboxData.letterboxPositionForHorizontalReachability;
mLetterboxPositionForVerticalReachability =
letterboxData.letterboxPositionForVerticalReachability;
+ mLetterboxPositionForBookModeReachability =
+ letterboxData.letterboxPositionForBookModeReachability;
+ mLetterboxPositionForTabletopModeReachability =
+ letterboxData.letterboxPositionForTabletopModeReachability;
} catch (IOException ioe) {
Slog.e(TAG,
"Error reading from LetterboxConfigurationPersister. "
@@ -192,6 +251,8 @@
mPersisterQueue.addItem(new UpdateValuesCommand(mConfigurationFile,
mLetterboxPositionForHorizontalReachability,
mLetterboxPositionForVerticalReachability,
+ mLetterboxPositionForBookModeReachability,
+ mLetterboxPositionForTabletopModeReachability,
mCompletionCallback), /* flush */ true);
}
@@ -221,13 +282,18 @@
private final int mHorizontalReachability;
private final int mVerticalReachability;
+ private final int mBookModeReachability;
+ private final int mTabletopModeReachability;
UpdateValuesCommand(@NonNull AtomicFile fileToUpdate,
int horizontalReachability, int verticalReachability,
+ int bookModeReachability, int tabletopModeReachability,
@Nullable Consumer<String> onComplete) {
mFileToUpdate = fileToUpdate;
mHorizontalReachability = horizontalReachability;
mVerticalReachability = verticalReachability;
+ mBookModeReachability = bookModeReachability;
+ mTabletopModeReachability = tabletopModeReachability;
mOnComplete = onComplete;
}
@@ -237,6 +303,10 @@
new WindowManagerProtos.LetterboxProto();
letterboxData.letterboxPositionForHorizontalReachability = mHorizontalReachability;
letterboxData.letterboxPositionForVerticalReachability = mVerticalReachability;
+ letterboxData.letterboxPositionForBookModeReachability =
+ mBookModeReachability;
+ letterboxData.letterboxPositionForTabletopModeReachability =
+ mTabletopModeReachability;
final byte[] bytes = WindowManagerProtos.LetterboxProto.toByteArray(letterboxData);
FileOutputStream fos = null;
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index a53a5fc..fd7e082 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -80,6 +80,7 @@
// SizeCompatTests and LetterboxTests but not all.
// TODO(b/185264020): Consider making LetterboxUiController applicable to any level of the
// hierarchy in addition to ActivityRecord (Task, DisplayArea, ...).
+// TODO(b/263021211): Consider renaming to more generic CompatUIController.
final class LetterboxUiController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
@@ -125,6 +126,11 @@
@Nullable
private Letterbox mLetterbox;
+ // Whether activity "refresh" was requested but not finished in
+ // ActivityRecord#activityResumedLocked following the camera compat force rotation in
+ // DisplayRotationCompatPolicy.
+ private boolean mIsRefreshAfterRotationRequested;
+
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
mLetterboxConfiguration = wmService.mLetterboxConfiguration;
// Given activityRecord may not be fully constructed since LetterboxUiController
@@ -147,6 +153,18 @@
}
}
+ /**
+ * Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked}
+ * following the camera compat force rotation in {@link DisplayRotationCompatPolicy}.
+ */
+ boolean isRefreshAfterRotationRequested() {
+ return mIsRefreshAfterRotationRequested;
+ }
+
+ void setIsRefreshAfterRotationRequested(boolean isRequested) {
+ mIsRefreshAfterRotationRequested = isRequested;
+ }
+
boolean hasWallpaperBackgroundForLetterbox() {
return mShowWallpaperForLetterboxBackground;
}
@@ -275,30 +293,62 @@
&& mActivityRecord.fillsParent();
}
+ // Check if we are in the given pose and in fullscreen mode.
+ // 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.FoldState state,
+ boolean isTabletop) {
+ Task task = mActivityRecord.getTask();
+ return mActivityRecord.mDisplayContent != null
+ && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(state,
+ isTabletop)
+ && task != null
+ && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+ }
+
+ // 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 isDisplayFullScreenAndSeparatingHinge() {
+ Task task = mActivityRecord.getTask();
+ return mActivityRecord.mDisplayContent != null
+ && mActivityRecord.mDisplayContent.getDisplayRotation().isDisplaySeparatingHinge()
+ && task != null
+ && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+ }
+
+
float getHorizontalPositionMultiplier(Configuration parentConfiguration) {
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
+ boolean bookMode = isDisplayFullScreenAndInPosture(
+ DeviceStateController.FoldState.HALF_FOLDED, false /* isTabletop */);
return isHorizontalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
- ? mLetterboxConfiguration.getHorizontalMultiplierForReachability()
- : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier();
+ ? mLetterboxConfiguration.getHorizontalMultiplierForReachability(bookMode)
+ : mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(bookMode);
}
float getVerticalPositionMultiplier(Configuration parentConfiguration) {
// Don't check resolved configuration because it may not be updated yet during
// configuration change.
+ boolean tabletopMode = isDisplayFullScreenAndInPosture(
+ DeviceStateController.FoldState.HALF_FOLDED, true /* isTabletop */);
return isVerticalReachabilityEnabled(parentConfiguration)
// Using the last global dynamic position to avoid "jumps" when moving
// between apps or activities.
- ? mLetterboxConfiguration.getVerticalMultiplierForReachability()
- : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier();
+ ? mLetterboxConfiguration.getVerticalMultiplierForReachability(tabletopMode)
+ : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(tabletopMode);
}
float getFixedOrientationLetterboxAspectRatio() {
- return mActivityRecord.shouldCreateCompatDisplayInsets()
- ? getDefaultMinAspectRatioForUnresizableApps()
- : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+ return isDisplayFullScreenAndSeparatingHinge()
+ ? getSplitScreenAspectRatio()
+ : mActivityRecord.shouldCreateCompatDisplayInsets()
+ ? getDefaultMinAspectRatioForUnresizableApps()
+ : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
}
private float getDefaultMinAspectRatioForUnresizableApps() {
@@ -351,11 +401,13 @@
return;
}
+ boolean isInFullScreenBookMode = isDisplayFullScreenAndSeparatingHinge();
int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
- .getLetterboxPositionForHorizontalReachability();
+ .getLetterboxPositionForHorizontalReachability(isInFullScreenBookMode);
if (mLetterbox.getInnerFrame().left > x) {
// Moving to the next stop on the left side of the app window: right > center > left.
- mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop();
+ mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop(
+ isInFullScreenBookMode);
int changeToLog =
letterboxPositionForHorizontalReachability
== LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
@@ -364,7 +416,8 @@
logLetterboxPositionChange(changeToLog);
} else if (mLetterbox.getInnerFrame().right < x) {
// Moving to the next stop on the right side of the app window: left > center > right.
- mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
+ mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+ isInFullScreenBookMode);
int changeToLog =
letterboxPositionForHorizontalReachability
== LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
@@ -388,11 +441,13 @@
// Only react to clicks at the top and bottom of the letterboxed app window.
return;
}
+ boolean isInFullScreenTabletopMode = isDisplayFullScreenAndSeparatingHinge();
int letterboxPositionForVerticalReachability = mLetterboxConfiguration
- .getLetterboxPositionForVerticalReachability();
+ .getLetterboxPositionForVerticalReachability(isInFullScreenTabletopMode);
if (mLetterbox.getInnerFrame().top > y) {
// Moving to the next stop on the top side of the app window: bottom > center > top.
- mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop();
+ mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop(
+ isInFullScreenTabletopMode);
int changeToLog =
letterboxPositionForVerticalReachability
== LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
@@ -401,7 +456,8 @@
logLetterboxPositionChange(changeToLog);
} else if (mLetterbox.getInnerFrame().bottom < y) {
// Moving to the next stop on the bottom side of the app window: top > center > bottom.
- mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
+ mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+ isInFullScreenTabletopMode);
int changeToLog =
letterboxPositionForVerticalReachability
== LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
@@ -712,10 +768,10 @@
+ getVerticalPositionMultiplier(mActivityRecord.getParent().getConfiguration()));
pw.println(prefix + " letterboxPositionForHorizontalReachability="
+ LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
- mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+ mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
pw.println(prefix + " letterboxPositionForVerticalReachability="
+ LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
- mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
+ mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
pw.println(prefix + " fixedOrientationLetterboxAspectRatio="
+ mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
pw.println(prefix + " defaultMinAspectRatioForUnresizableApps="
@@ -780,14 +836,20 @@
int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
if (isHorizontalReachabilityEnabled()) {
int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
- .getLetterboxPositionForHorizontalReachability();
+ .getLetterboxPositionForHorizontalReachability(
+ isDisplayFullScreenAndInPosture(
+ DeviceStateController.FoldState.HALF_FOLDED,
+ false /* isTabletop */));
positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
- letterboxPositionForHorizontalReachability);
+ letterboxPositionForHorizontalReachability);
} else if (isVerticalReachabilityEnabled()) {
int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
- .getLetterboxPositionForVerticalReachability();
+ .getLetterboxPositionForVerticalReachability(
+ isDisplayFullScreenAndInPosture(
+ DeviceStateController.FoldState.HALF_FOLDED,
+ true /* isTabletop */));
positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
- letterboxPositionForVerticalReachability);
+ letterboxPositionForVerticalReachability);
}
return positionToLog;
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 4e692e2d..aef6d1d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -53,6 +53,7 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
@@ -806,54 +807,6 @@
return 0;
}
- private int runSetLetterboxIsHorizontalReachabilityEnabled(PrintWriter pw)
- throws RemoteException {
- String arg = getNextArg();
- final boolean enabled;
- switch (arg) {
- case "true":
- case "1":
- enabled = true;
- break;
- case "false":
- case "0":
- enabled = false;
- break;
- default:
- getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
- return -1;
- }
-
- synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(enabled);
- }
- return 0;
- }
-
- private int runSetLetterboxIsVerticalReachabilityEnabled(PrintWriter pw)
- throws RemoteException {
- String arg = getNextArg();
- final boolean enabled;
- switch (arg) {
- case "true":
- case "1":
- enabled = true;
- break;
- case "false":
- case "0":
- enabled = false;
- break;
- default:
- getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
- return -1;
- }
-
- synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsVerticalReachabilityEnabled(enabled);
- }
- return 0;
- }
-
private int runSetLetterboxDefaultPositionForHorizontalReachability(PrintWriter pw)
throws RemoteException {
@LetterboxHorizontalReachabilityPosition final int position;
@@ -916,32 +869,13 @@
return 0;
}
- private int runSetLetterboxIsEducationEnabled(PrintWriter pw) throws RemoteException {
- String arg = getNextArg();
- final boolean enabled;
- switch (arg) {
- case "true":
- case "1":
- enabled = true;
- break;
- case "false":
- case "0":
- enabled = false;
- break;
- default:
- getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
- return -1;
- }
-
- synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsEducationEnabled(enabled);
- }
- return 0;
- }
-
- private int runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(PrintWriter pw)
+ private int runSetBooleanFlag(PrintWriter pw, Consumer<Boolean> setter)
throws RemoteException {
String arg = getNextArg();
+ if (arg == null) {
+ getErrPrintWriter().println("Error: expected true, 1, false, 0, but got empty input.");
+ return -1;
+ }
final boolean enabled;
switch (arg) {
case "true":
@@ -958,30 +892,7 @@
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsSplitScreenAspectRatioForUnresizableAppsEnabled(enabled);
- }
- return 0;
- }
-
- private int runSetTranslucentLetterboxingEnabled(PrintWriter pw) {
- String arg = getNextArg();
- final boolean enabled;
- switch (arg) {
- case "true":
- case "1":
- enabled = true;
- break;
- case "false":
- case "0":
- enabled = false;
- break;
- default:
- getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
- return -1;
- }
-
- synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(enabled);
+ setter.accept(enabled);
}
return 0;
}
@@ -1024,10 +935,12 @@
runSetLetterboxVerticalPositionMultiplier(pw);
break;
case "--isHorizontalReachabilityEnabled":
- runSetLetterboxIsHorizontalReachabilityEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsHorizontalReachabilityEnabled);
break;
case "--isVerticalReachabilityEnabled":
- runSetLetterboxIsVerticalReachabilityEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsVerticalReachabilityEnabled);
break;
case "--defaultPositionForHorizontalReachability":
runSetLetterboxDefaultPositionForHorizontalReachability(pw);
@@ -1036,13 +949,23 @@
runSetLetterboxDefaultPositionForVerticalReachability(pw);
break;
case "--isEducationEnabled":
- runSetLetterboxIsEducationEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration::setIsEducationEnabled);
break;
case "--isSplitScreenAspectRatioForUnresizableAppsEnabled":
- runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsSplitScreenAspectRatioForUnresizableAppsEnabled);
break;
case "--isTranslucentLetterboxingEnabled":
- runSetTranslucentLetterboxingEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setTranslucentLetterboxingOverrideEnabled);
+ break;
+ case "--isCameraCompatRefreshEnabled":
+ runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
+ .setCameraCompatRefreshEnabled(enabled));
+ break;
+ case "--isCameraCompatRefreshCycleThroughStopEnabled":
+ runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
+ .setCameraCompatRefreshCycleThroughStopEnabled(enabled));
break;
default:
getErrPrintWriter().println(
@@ -1089,27 +1012,34 @@
mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier();
break;
case "isHorizontalReachabilityEnabled":
- mLetterboxConfiguration.getIsHorizontalReachabilityEnabled();
+ mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled();
break;
case "isVerticalReachabilityEnabled":
- mLetterboxConfiguration.getIsVerticalReachabilityEnabled();
+ mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
break;
case "defaultPositionForHorizontalReachability":
- mLetterboxConfiguration.getDefaultPositionForHorizontalReachability();
+ mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability();
break;
case "defaultPositionForVerticalReachability":
- mLetterboxConfiguration.getDefaultPositionForVerticalReachability();
+ mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
break;
case "isEducationEnabled":
- mLetterboxConfiguration.getIsEducationEnabled();
+ mLetterboxConfiguration.resetIsEducationEnabled();
break;
case "isSplitScreenAspectRatioForUnresizableAppsEnabled":
mLetterboxConfiguration
- .getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+ .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
break;
case "isTranslucentLetterboxingEnabled":
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
break;
+ case "isCameraCompatRefreshEnabled":
+ mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+ break;
+ case "isCameraCompatRefreshCycleThroughStopEnabled":
+ mLetterboxConfiguration
+ .resetCameraCompatRefreshCycleThroughStopEnabled();
+ break;
default:
getErrPrintWriter().println(
"Error: Unrecognized letterbox style option: " + arg);
@@ -1211,6 +1141,8 @@
mLetterboxConfiguration.resetIsEducationEnabled();
mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+ mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+ mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
}
}
@@ -1219,9 +1151,17 @@
pw.println("Corner radius: "
+ mLetterboxConfiguration.getLetterboxActivityCornersRadius());
pw.println("Horizontal position multiplier: "
- + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier());
+ + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(
+ false /* isInBookMode */));
pw.println("Vertical position multiplier: "
- + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier());
+ + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(
+ false /* isInTabletopMode */));
+ pw.println("Horizontal position multiplier (book mode): "
+ + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier(
+ true /* isInBookMode */));
+ pw.println("Vertical position multiplier (tabletop mode): "
+ + mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier(
+ true /* isInTabletopMode */));
pw.println("Aspect ratio: "
+ mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
pw.println("Default min aspect ratio for unresizable apps: "
@@ -1238,15 +1178,21 @@
mLetterboxConfiguration.getDefaultPositionForVerticalReachability()));
pw.println("Current position for horizontal reachability:"
+ LetterboxConfiguration.letterboxHorizontalReachabilityPositionToString(
- mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability()));
+ mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(false)));
pw.println("Current position for vertical reachability:"
+ LetterboxConfiguration.letterboxVerticalReachabilityPositionToString(
- mLetterboxConfiguration.getLetterboxPositionForVerticalReachability()));
+ mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(false)));
pw.println("Is education enabled: "
+ mLetterboxConfiguration.getIsEducationEnabled());
pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
+ mLetterboxConfiguration
.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
+
+ pw.println(" Is activity \"refresh\" in camera compatibility treatment enabled: "
+ + mLetterboxConfiguration.isCameraCompatRefreshEnabled());
+ pw.println(" Refresh using \"stopped -> resumed\" cycle: "
+ + mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled());
+
pw.println("Background type: "
+ LetterboxConfiguration.letterboxBackgroundTypeToString(
mLetterboxConfiguration.getLetterboxBackgroundType()));
@@ -1456,7 +1402,12 @@
pw.println(" unresizable apps.");
pw.println(" --isTranslucentLetterboxingEnabled [true|1|false|0]");
pw.println(" Whether letterboxing for translucent activities is enabled.");
-
+ pw.println(" --isCameraCompatRefreshEnabled [true|1|false|0]");
+ pw.println(" Whether camera compatibility refresh is enabled.");
+ pw.println(" --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]");
+ pw.println(" Whether activity \"refresh\" in camera compatibility treatment should");
+ pw.println(" happen using the \"stopped -> resumed\" cycle rather than");
+ pw.println(" \"paused -> resumed\" cycle.");
pw.println(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
pw.println(" |horizontalPositionMultiplier|verticalPositionMultiplier");
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 7bc8931..f628fba 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -74,6 +74,9 @@
<xs:element type="sensorDetails" name="lightSensor">
<xs:annotation name="final"/>
</xs:element>
+ <xs:element type="sensorDetails" name="screenOffBrightnessSensor">
+ <xs:annotation name="final"/>
+ </xs:element>
<xs:element type="sensorDetails" name="proxSensor">
<xs:annotation name="final"/>
</xs:element>
@@ -109,6 +112,11 @@
<xs:element type="thresholds" name="ambientBrightnessChangeThresholdsIdle" minOccurs="0" maxOccurs="1">
<xs:annotation name="final"/>
</xs:element>
+ <!-- Table that translates sensor values from the screenOffBrightnessSensor
+ to lux values; -1 means the lux reading is not available. -->
+ <xs:element type="integer-array" name="screenOffBrightnessSensorValueToLux">
+ <xs:annotation name="final"/>
+ </xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
@@ -485,4 +493,10 @@
</xs:element>
</xs:sequence>
</xs:complexType>
+
+ <xs:complexType name="integer-array">
+ <xs:sequence>
+ <xs:element name="item" type="xs:nonNegativeInteger" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
</xs:schema>
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 6276eda..cb08179 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -98,6 +98,8 @@
method public final java.math.BigInteger getScreenBrightnessRampIncreaseMaxMillis();
method public final java.math.BigDecimal getScreenBrightnessRampSlowDecrease();
method public final java.math.BigDecimal getScreenBrightnessRampSlowIncrease();
+ method public final com.android.server.display.config.SensorDetails getScreenOffBrightnessSensor();
+ method public final com.android.server.display.config.IntegerArray getScreenOffBrightnessSensorValueToLux();
method @NonNull public final com.android.server.display.config.ThermalThrottling getThermalThrottling();
method public final void setAmbientBrightnessChangeThresholds(@NonNull com.android.server.display.config.Thresholds);
method public final void setAmbientBrightnessChangeThresholdsIdle(com.android.server.display.config.Thresholds);
@@ -120,6 +122,8 @@
method public final void setScreenBrightnessRampIncreaseMaxMillis(java.math.BigInteger);
method public final void setScreenBrightnessRampSlowDecrease(java.math.BigDecimal);
method public final void setScreenBrightnessRampSlowIncrease(java.math.BigDecimal);
+ method public final void setScreenOffBrightnessSensor(com.android.server.display.config.SensorDetails);
+ method public final void setScreenOffBrightnessSensorValueToLux(com.android.server.display.config.IntegerArray);
method public final void setThermalThrottling(@NonNull com.android.server.display.config.ThermalThrottling);
}
@@ -160,6 +164,11 @@
method public final void setTransitionPoint_all(@NonNull java.math.BigDecimal);
}
+ public class IntegerArray {
+ ctor public IntegerArray();
+ method public java.util.List<java.math.BigInteger> getItem();
+ }
+
public class NitsMap {
ctor public NitsMap();
method public String getInterpolation();
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 6b705aa..86c5937 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -161,6 +161,14 @@
assertArrayEquals(new int[]{70, 80},
mDisplayDeviceConfig.getHighAmbientBrightnessThresholds());
+ assertEquals("sensor_12345",
+ mDisplayDeviceConfig.getScreenOffBrightnessSensor().type);
+ assertEquals("Sensor 12345",
+ mDisplayDeviceConfig.getScreenOffBrightnessSensor().name);
+
+ assertArrayEquals(new int[]{-1, 10, 20, 30, 40},
+ mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux());
+
// Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
// HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
}
@@ -232,6 +240,7 @@
HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
assertArrayEquals(mDisplayDeviceConfig.getHighAmbientBrightnessThresholds(),
HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+
// Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
// HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
}
@@ -283,6 +292,10 @@
+ "<thermalStatusLimit>light</thermalStatusLimit>\n"
+ "<allowInLowPowerMode>false</allowInLowPowerMode>\n"
+ "</highBrightnessMode>\n"
+ + "<screenOffBrightnessSensor>\n"
+ + "<type>sensor_12345</type>\n"
+ + "<name>Sensor 12345</name>\n"
+ + "</screenOffBrightnessSensor>\n"
+ "<ambientBrightnessChangeThresholds>\n"
+ "<brighteningThresholds>\n"
+ "<minimum>10</minimum>\n"
@@ -467,6 +480,13 @@
+ "</blockingZoneThreshold>\n"
+ "</higherBlockingZoneConfigs>\n"
+ "</refreshRate>\n"
+ + "<screenOffBrightnessSensorValueToLux>\n"
+ + "<item>-1</item>\n"
+ + "<item>10</item>\n"
+ + "<item>20</item>\n"
+ + "<item>30</item>\n"
+ + "<item>40</item>\n"
+ + "</screenOffBrightnessSensorValueToLux>\n"
+ "</displayConfiguration>\n";
}
@@ -544,6 +564,7 @@
when(mResources.getIntArray(
R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
.thenReturn(HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+
mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
}
diff --git a/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java b/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
new file mode 100644
index 0000000..ea04a19
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.testutils.OffsettableClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ScreenOffBrightnessSensorControllerTest {
+
+ private static final int[] SENSOR_TO_LUX = new int[]{-1, 10, 20, 30, 40};
+
+ private ScreenOffBrightnessSensorController mController;
+ private OffsettableClock mClock;
+ private Sensor mLightSensor;
+
+ @Mock SensorManager mSensorManager;
+ @Mock Handler mNoOpHandler;
+ @Mock BrightnessMappingStrategy mBrightnessMappingStrategy;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mClock = new OffsettableClock.Stopped();
+ mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor");
+ mController = new ScreenOffBrightnessSensorController(
+ mSensorManager,
+ mLightSensor,
+ mNoOpHandler,
+ mClock::now,
+ SENSOR_TO_LUX,
+ mBrightnessMappingStrategy
+ );
+ }
+
+ @Test
+ public void testBrightness() throws Exception {
+ when(mSensorManager.registerListener(any(SensorEventListener.class), eq(mLightSensor),
+ eq(SensorManager.SENSOR_DELAY_NORMAL), any(Handler.class)))
+ .thenReturn(true);
+ mController.setLightSensorEnabled(true);
+ ArgumentCaptor<SensorEventListener> listenerCaptor =
+ ArgumentCaptor.forClass(SensorEventListener.class);
+ verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+ eq(SensorManager.SENSOR_DELAY_NORMAL), any(Handler.class));
+ SensorEventListener listener = listenerCaptor.getValue();
+
+ listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+ assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+ mController.getAutomaticScreenBrightness(), 0);
+
+ int sensorValue = 1;
+ float brightness = 0.2f;
+ listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+ when(mBrightnessMappingStrategy.getBrightness(SENSOR_TO_LUX[sensorValue]))
+ .thenReturn(brightness);
+ assertEquals(brightness, mController.getAutomaticScreenBrightness(), 0);
+
+ sensorValue = 2;
+ brightness = 0.4f;
+ listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+ when(mBrightnessMappingStrategy.getBrightness(SENSOR_TO_LUX[sensorValue]))
+ .thenReturn(brightness);
+ assertEquals(brightness, mController.getAutomaticScreenBrightness(), 0);
+
+ sensorValue = 3;
+ brightness = 0.6f;
+ listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+ when(mBrightnessMappingStrategy.getBrightness(SENSOR_TO_LUX[sensorValue]))
+ .thenReturn(brightness);
+ assertEquals(brightness, mController.getAutomaticScreenBrightness(), 0);
+
+ sensorValue = 4;
+ brightness = 0.8f;
+ listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+ when(mBrightnessMappingStrategy.getBrightness(SENSOR_TO_LUX[sensorValue]))
+ .thenReturn(brightness);
+ assertEquals(brightness, mController.getAutomaticScreenBrightness(), 0);
+
+ sensorValue = 5;
+ listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, sensorValue));
+ assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+ mController.getAutomaticScreenBrightness(), 0);
+ }
+
+ @Test
+ public void testSensorValueValidTime() throws Exception {
+ when(mSensorManager.registerListener(any(SensorEventListener.class), eq(mLightSensor),
+ eq(SensorManager.SENSOR_DELAY_NORMAL), any(Handler.class)))
+ .thenReturn(true);
+ mController.setLightSensorEnabled(true);
+ ArgumentCaptor<SensorEventListener> listenerCaptor =
+ ArgumentCaptor.forClass(SensorEventListener.class);
+ verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+ eq(SensorManager.SENSOR_DELAY_NORMAL), any(Handler.class));
+ SensorEventListener listener = listenerCaptor.getValue();
+
+ listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1));
+ mController.setLightSensorEnabled(false);
+ assertNotEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+ mController.getAutomaticScreenBrightness(), 0);
+
+ mClock.fastForward(2000);
+ mController.setLightSensorEnabled(false);
+ assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT,
+ mController.getAutomaticScreenBrightness(), 0);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/TestUtils.java b/services/tests/servicestests/src/com/android/server/display/TestUtils.java
index 0454587..f3f04b8 100644
--- a/services/tests/servicestests/src/com/android/server/display/TestUtils.java
+++ b/services/tests/servicestests/src/com/android/server/display/TestUtils.java
@@ -28,13 +28,13 @@
public final class TestUtils {
- public static SensorEvent createSensorEvent(Sensor sensor, int lux) throws Exception {
+ public static SensorEvent createSensorEvent(Sensor sensor, int value) throws Exception {
final Constructor<SensorEvent> constructor =
SensorEvent.class.getDeclaredConstructor(int.class);
constructor.setAccessible(true);
final SensorEvent event = constructor.newInstance(1);
event.sensor = sensor;
- event.values[0] = lux;
+ event.values[0] = value;
event.timestamp = SystemClock.elapsedRealtimeNanos();
return event;
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index b16ca8b..b4a294d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -19,6 +19,7 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;
+import android.os.Parcel;
import android.service.notification.ZenPolicy;
import android.service.notification.nano.DNDPolicyProto;
import android.test.suitebuilder.annotation.SmallTest;
@@ -32,9 +33,13 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ZenPolicyTest extends UiServiceTestCase {
+ private static final String CLASS = "android.service.notification.ZenPolicy";
@Test
public void testZenPolicyApplyAllowedToDisallowed() {
@@ -524,6 +529,66 @@
assertProtoMatches(policy, policy.toProto());
}
+ @Test
+ public void testTooLongLists_fromParcel() {
+ ArrayList<Integer> longList = new ArrayList<Integer>(50);
+ for (int i = 0; i < 50; i++) {
+ longList.add(ZenPolicy.STATE_UNSET);
+ }
+
+ ZenPolicy.Builder builder = new ZenPolicy.Builder();
+ ZenPolicy policy = builder.build();
+
+ try {
+ Field priorityCategories = Class.forName(CLASS).getDeclaredField(
+ "mPriorityCategories");
+ priorityCategories.setAccessible(true);
+ priorityCategories.set(policy, longList);
+
+ Field visualEffects = Class.forName(CLASS).getDeclaredField("mVisualEffects");
+ visualEffects.setAccessible(true);
+ visualEffects.set(policy, longList);
+ } catch (NoSuchFieldException e) {
+ fail(e.toString());
+ } catch (ClassNotFoundException e) {
+ fail(e.toString());
+ } catch (IllegalAccessException e) {
+ fail(e.toString());
+ }
+
+ Parcel parcel = Parcel.obtain();
+ policy.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel);
+
+ // Confirm that all the fields are accessible and UNSET
+ assertAllPriorityCategoriesUnsetExcept(fromParcel, -1);
+ assertAllVisualEffectsUnsetExcept(fromParcel, -1);
+
+ // Because we don't access the lists directly, we also need to use reflection to make sure
+ // the lists are the right length.
+ try {
+ Field priorityCategories = Class.forName(CLASS).getDeclaredField(
+ "mPriorityCategories");
+ priorityCategories.setAccessible(true);
+ ArrayList<Integer> pcList = (ArrayList<Integer>) priorityCategories.get(fromParcel);
+ assertEquals(ZenPolicy.NUM_PRIORITY_CATEGORIES, pcList.size());
+
+
+ Field visualEffects = Class.forName(CLASS).getDeclaredField("mVisualEffects");
+ visualEffects.setAccessible(true);
+ ArrayList<Integer> veList = (ArrayList<Integer>) visualEffects.get(fromParcel);
+ assertEquals(ZenPolicy.NUM_VISUAL_EFFECTS, veList.size());
+ } catch (NoSuchFieldException e) {
+ fail(e.toString());
+ } catch (ClassNotFoundException e) {
+ fail(e.toString());
+ } catch (IllegalAccessException e) {
+ fail(e.toString());
+ }
+ }
+
private void assertAllPriorityCategoriesUnsetExcept(ZenPolicy policy, int except) {
if (except != ZenPolicy.PRIORITY_CATEGORY_REMINDERS) {
assertEquals(ZenPolicy.STATE_UNSET, policy.getPriorityCategoryReminders());
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
new file mode 100644
index 0000000..d1234e3
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2022 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;
+
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_90;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.RefreshCallbackItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.content.res.Configuration.Orientation;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+import android.view.Surface.Rotation;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for {@link DisplayRotationCompatPolicy}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:DisplayRotationCompatPolicyTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public final class DisplayRotationCompatPolicyTests extends WindowTestsBase {
+
+ private static final String TEST_PACKAGE_1 = "com.test.package.one";
+ private static final String TEST_PACKAGE_2 = "com.test.package.two";
+ private static final String CAMERA_ID_1 = "camera-1";
+ private static final String CAMERA_ID_2 = "camera-2";
+
+ private CameraManager mMockCameraManager;
+ private Handler mMockHandler;
+ private LetterboxConfiguration mLetterboxConfiguration;
+
+ private DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
+ private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
+
+ private ActivityRecord mActivity;
+ private Task mTask;
+
+ @Before
+ public void setUp() throws Exception {
+ mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
+ spyOn(mLetterboxConfiguration);
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ anyBoolean()))
+ .thenReturn(true);
+ when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+ .thenReturn(true);
+ when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+ .thenReturn(true);
+
+ mMockCameraManager = mock(CameraManager.class);
+ doAnswer(invocation -> {
+ mCameraAvailabilityCallback = invocation.getArgument(1);
+ return null;
+ }).when(mMockCameraManager).registerAvailabilityCallback(
+ any(Executor.class), any(CameraManager.AvailabilityCallback.class));
+
+ spyOn(mContext);
+ when(mContext.getSystemService(CameraManager.class)).thenReturn(mMockCameraManager);
+
+ spyOn(mDisplayContent);
+
+ mDisplayContent.setIgnoreOrientationRequest(true);
+
+ mMockHandler = mock(Handler.class);
+
+ when(mMockHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
+ invocation -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ });
+ mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(
+ mDisplayContent, mMockHandler);
+ }
+
+ @Test
+ public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ anyBoolean()))
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true))
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent);
+ mActivity.getTask().reparent(organizer.mPrimary, WindowContainer.POSITION_TOP,
+ false /* moveParents */, "test" /* reason */);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertTrue(mActivity.inMultiWindowMode());
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationUnspecified_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationLocked_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_LOCKED);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationNoSensor_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_NOSENSOR);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testIgnoreOrientationRequestIsFalse_noForceRotationOrRefresh() throws Exception {
+ mDisplayContent.setIgnoreOrientationRequest(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testDisplayNotInternal_noForceRotationOrRefresh() throws Exception {
+ Display display = mDisplayContent.getDisplay();
+ spyOn(display);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ when(display.getType()).thenReturn(Display.TYPE_EXTERNAL);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_WIFI);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_OVERLAY);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_VIRTUAL);
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testNoCameraConnection_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testCameraReconnected_forceRotationAndRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+ assertActivityRefreshRequested(/* refreshRequested */ true);
+ }
+
+ @Test
+ public void testReconnectedToDifferentCamera_forceRotationAndRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+ assertActivityRefreshRequested(/* refreshRequested */ true);
+ }
+
+ @Test
+ public void testGetOrientation_cameraConnectionClosed_returnUnspecified() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ @Test
+ public void testCameraOpenedForDifferentPackage_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testGetOrientation_portraitActivity_portraitNaturalOrientation_returnPortrait() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT,
+ /* naturalOrientation */ ORIENTATION_PORTRAIT,
+ /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ @Test
+ public void testGetOrientation_portraitActivity_landscapeNaturalOrientation_returnLandscape() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT,
+ /* naturalOrientation */ ORIENTATION_LANDSCAPE,
+ /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ @Test
+ public void testGetOrientation_landscapeActivity_portraitNaturalOrientation_returnLandscape() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* naturalOrientation */ ORIENTATION_PORTRAIT,
+ /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ @Test
+ public void testGetOrientation_landscapeActivity_landscapeNaturalOrientation_returnPortrait() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* naturalOrientation */ ORIENTATION_LANDSCAPE,
+ /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ private void testGetOrientationForActivityAndNaturalOrientations(
+ @ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation,
+ @ScreenOrientation int expectedOrientation) {
+ configureActivityAndDisplay(activityOrientation, naturalOrientation);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ expectedOrientation);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_displayRotationNotChanging_noRefresh()
+ throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false);
+
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation) {
+ configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT);
+ }
+
+ private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation) {
+
+ mTask = new TaskBuilder(mSupervisor)
+ .setDisplay(mDisplayContent)
+ .build();
+
+ mActivity = new ActivityBuilder(mAtm)
+ .setComponent(new ComponentName(TEST_PACKAGE_1, ".TestActivity"))
+ .setScreenOrientation(activityOrientation)
+ .setTask(mTask)
+ .build();
+
+ spyOn(mActivity.mAtmService.getLifecycleManager());
+ spyOn(mActivity.mLetterboxUiController);
+
+ doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean());
+ doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation();
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception {
+ assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested,
+ boolean cycleThroughStop) throws Exception {
+ verify(mActivity.mLetterboxUiController, times(refreshRequested ? 1 : 0))
+ .setIsRefreshAfterRotationRequested(true);
+
+ final ClientTransaction transaction = ClientTransaction.obtain(
+ mActivity.app.getThread(), mActivity.token);
+ transaction.addCallback(RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
+ transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false));
+
+ verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
+ .scheduleTransaction(eq(transaction));
+ }
+
+ private void assertNoForceRotationOrRefresh() throws Exception {
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ private void callOnActivityConfigurationChanging(
+ ActivityRecord activity, boolean isDisplayRotationChanging) {
+ mDisplayRotationCompatPolicy.onActivityConfigurationChanging(activity,
+ /* newConfig */ createConfigurationWithDisplayRotation(ROTATION_0),
+ /* newConfig */ createConfigurationWithDisplayRotation(
+ isDisplayRotationChanging ? ROTATION_90 : ROTATION_0));
+ }
+
+ private static Configuration createConfigurationWithDisplayRotation(@Rotation int rotation) {
+ final Configuration config = new Configuration();
+ config.windowConfiguration.setDisplayRotation(rotation);
+ return config;
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index b45c37f..491f876d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -60,6 +60,7 @@
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
+import android.view.DisplayAddress;
import android.view.Surface;
import android.view.WindowManager;
@@ -101,6 +102,7 @@
private static WindowManagerService sMockWm;
private DisplayContent mMockDisplayContent;
private DisplayPolicy mMockDisplayPolicy;
+ private DisplayAddress mMockDisplayAddress;
private Context mMockContext;
private Resources mMockRes;
private SensorManager mMockSensorManager;
@@ -1091,9 +1093,11 @@
when(mMockResolver.acquireProvider(Settings.AUTHORITY))
.thenReturn(mFakeSettingsProvider.getIContentProvider());
+ mMockDisplayAddress = mock(DisplayAddress.class);
+
mMockDisplayWindowSettings = mock(DisplayWindowSettings.class);
- mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayPolicy,
- mMockDisplayWindowSettings, mMockContext, new Object());
+ mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress,
+ mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object());
reset(sMockWm);
captureObservers();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
index 1be9de7..51a7e74 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
@@ -67,6 +67,11 @@
R.integer.config_letterboxDefaultPositionForHorizontalReachability),
() -> mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForVerticalReachability),
+ () -> mContext.getResources().getInteger(
+ R.integer.config_letterboxDefaultPositionForBookModeReachability),
+ () -> mContext.getResources().getInteger(
+ R.integer.config_letterboxDefaultPositionForTabletopModeReachability
+ ),
mConfigFolder, mPersisterQueue, mQueueState);
mQueueListener = queueEmpty -> mQueueState.onItemAdded();
mPersisterQueue.addListener(mQueueListener);
@@ -84,14 +89,15 @@
@Test
public void test_whenStoreIsCreated_valuesAreDefaults() {
final int positionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ false);
final int defaultPositionForHorizontalReachability =
mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForHorizontalReachability);
Assert.assertEquals(defaultPositionForHorizontalReachability,
positionForHorizontalReachability);
final int positionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
final int defaultPositionForVerticalReachability =
mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForVerticalReachability);
@@ -101,15 +107,16 @@
@Test
public void test_whenUpdatedWithNewValues_valuesAreWritten() {
- mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+ mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
- mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+ mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
waitForCompletion(mPersisterQueue);
final int newPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ false);
final int newPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
newPositionForHorizontalReachability);
Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -120,24 +127,24 @@
public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
final PersisterQueue firstPersisterQueue = new PersisterQueue();
final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
- mContext, () -> -1, () -> -1, mContext.getFilesDir(), firstPersisterQueue,
- mQueueState);
+ mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
+ firstPersisterQueue, mQueueState);
firstPersister.start();
- firstPersister.setLetterboxPositionForHorizontalReachability(
+ firstPersister.setLetterboxPositionForHorizontalReachability(false,
LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
- firstPersister.setLetterboxPositionForVerticalReachability(
+ firstPersister.setLetterboxPositionForVerticalReachability(false,
LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
waitForCompletion(firstPersisterQueue);
stopPersisterSafe(firstPersisterQueue);
final PersisterQueue secondPersisterQueue = new PersisterQueue();
final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
- mContext, () -> -1, () -> -1, mContext.getFilesDir(), secondPersisterQueue,
- mQueueState);
+ mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
+ secondPersisterQueue, mQueueState);
secondPersister.start();
final int newPositionForHorizontalReachability =
- secondPersister.getLetterboxPositionForHorizontalReachability();
+ secondPersister.getLetterboxPositionForHorizontalReachability(false);
final int newPositionForVerticalReachability =
- secondPersister.getLetterboxPositionForVerticalReachability();
+ secondPersister.getLetterboxPositionForVerticalReachability(false);
Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
newPositionForHorizontalReachability);
Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -149,15 +156,16 @@
@Test
public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
- mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+ mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
- mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+ mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
waitForCompletion(mPersisterQueue);
final int newPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ false);
final int newPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
newPositionForHorizontalReachability);
Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
@@ -165,14 +173,15 @@
deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
waitForCompletion(mPersisterQueue);
final int positionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
+ false);
final int defaultPositionForHorizontalReachability =
mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForHorizontalReachability);
Assert.assertEquals(defaultPositionForHorizontalReachability,
positionForHorizontalReachability);
final int positionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+ mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
final int defaultPositionForVerticalReachability =
mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForVerticalReachability);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
index c927f9e..e196704 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -26,6 +26,7 @@
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -39,7 +40,8 @@
import org.junit.Before;
import org.junit.Test;
-import java.util.function.Consumer;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
@SmallTest
@Presubmit
@@ -58,20 +60,28 @@
@Test
public void test_whenReadingValues_storeIsInvoked() {
- mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability();
- verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability();
- mLetterboxConfiguration.getLetterboxPositionForVerticalReachability();
- verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability();
+ for (boolean halfFoldPose : Arrays.asList(false, true)) {
+ mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose);
+ verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability(
+ halfFoldPose);
+ mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose);
+ verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability(
+ halfFoldPose);
+ }
}
@Test
public void test_whenSettingValues_updateConfigurationIsInvoked() {
- mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
- verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
- anyInt());
- mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
- verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
- anyInt());
+ for (boolean halfFoldPose : Arrays.asList(false, true)) {
+ mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop(
+ halfFoldPose);
+ verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+ eq(halfFoldPose), anyInt());
+ mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop(
+ halfFoldPose);
+ verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+ eq(halfFoldPose), anyInt());
+ }
}
@Test
@@ -81,33 +91,65 @@
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
// Starting from left
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
// Starting from right
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
assertForHorizontalMove(
/* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
/* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
+ LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+ // Starting from left - book mode
+ assertForHorizontalMove(
+ /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+ /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+ /* expectedTime */ 1,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+ assertForHorizontalMove(
+ /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+ /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+ /* expectedTime */ 1,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+ // Starting from right - book mode
+ assertForHorizontalMove(
+ /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+ /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+ /* expectedTime */ 2,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+ assertForHorizontalMove(
+ /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+ /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+ /* expectedTime */ 2,
+ /* halfFoldPose */ true,
LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
}
@@ -118,55 +160,87 @@
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
// Starting from top
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
/* expectedTime */ 1,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
// Starting from bottom
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
assertForVerticalMove(
/* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
/* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
/* expectedTime */ 2,
+ /* halfFoldPose */ false,
+ LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+ // Starting from top - tabletop mode
+ assertForVerticalMove(
+ /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+ /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+ /* expectedTime */ 1,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+ assertForVerticalMove(
+ /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+ /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+ /* expectedTime */ 1,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+ // Starting from bottom - tabletop mode
+ assertForVerticalMove(
+ /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+ /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+ /* expectedTime */ 2,
+ /* halfFoldPose */ true,
+ LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+ assertForVerticalMove(
+ /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+ /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+ /* expectedTime */ 2,
+ /* halfFoldPose */ true,
LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
}
private void assertForHorizontalMove(int from, int expected, int expectedTime,
- Consumer<LetterboxConfiguration> move) {
+ boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
// We are in the current position
- when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability())
+ when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability(halfFoldPose))
.thenReturn(from);
- move.accept(mLetterboxConfiguration);
+ move.accept(mLetterboxConfiguration, halfFoldPose);
verify(mLetterboxConfigurationPersister,
- times(expectedTime)).setLetterboxPositionForHorizontalReachability(
+ times(expectedTime)).setLetterboxPositionForHorizontalReachability(halfFoldPose,
expected);
}
private void assertForVerticalMove(int from, int expected, int expectedTime,
- Consumer<LetterboxConfiguration> move) {
+ boolean halfFoldPose, BiConsumer<LetterboxConfiguration, Boolean> move) {
// We are in the current position
- when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability())
+ when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability(halfFoldPose))
.thenReturn(from);
- move.accept(mLetterboxConfiguration);
+ move.accept(mLetterboxConfiguration, halfFoldPose);
verify(mLetterboxConfigurationPersister,
- times(expectedTime)).setLetterboxPositionForVerticalReachability(
+ times(expectedTime)).setLetterboxPositionForVerticalReachability(halfFoldPose,
expected);
}
}
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 94c33f2..7488e1c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
@@ -95,6 +96,7 @@
import com.android.internal.policy.SystemBarUtils;
import com.android.internal.statusbar.LetterboxDetails;
import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.wm.DeviceStateController.FoldState;
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -2821,6 +2823,70 @@
}
@Test
+ public void testUpdateResolvedBoundsVerticalPosition_tabletop() {
+
+ // Set up a display in portrait with a fixed-orientation LANDSCAPE app
+ setUpDisplaySizeWithApp(1400, 2800);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mActivity.mWmService.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(
+ 1.0f /*letterboxVerticalPositionMultiplier*/);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+ Rect letterboxNoFold = new Rect(0, 2100, 1400, 2800);
+ assertEquals(letterboxNoFold, mActivity.getBounds());
+
+ // Make the activity full-screen
+ mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ setFoldablePosture(true /* isHalfFolded */, true /* isTabletop */);
+
+ Rect letterboxHalfFold = new Rect(0, 0, 1400, 700);
+ assertEquals(letterboxHalfFold, mActivity.getBounds());
+
+ setFoldablePosture(false /* isHalfFolded */, false /* isTabletop */);
+
+ assertEquals(letterboxNoFold, mActivity.getBounds());
+
+ }
+
+ @Test
+ public void testUpdateResolvedBoundsHorizontalPosition_book() {
+
+ // Set up a display in landscape with a fixed-orientation PORTRAIT app
+ setUpDisplaySizeWithApp(2800, 1400);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mActivity.mWmService.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+ 1.0f /*letterboxVerticalPositionMultiplier*/);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+
+ Rect letterboxNoFold = new Rect(2100, 0, 2800, 1400);
+ assertEquals(letterboxNoFold, mActivity.getBounds());
+
+ // Make the activity full-screen
+ mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ setFoldablePosture(true /* isHalfFolded */, false /* isTabletop */);
+
+ Rect letterboxHalfFold = new Rect(0, 0, 700, 1400);
+ assertEquals(letterboxHalfFold, mActivity.getBounds());
+
+ setFoldablePosture(false /* isHalfFolded */, false /* isTabletop */);
+
+ assertEquals(letterboxNoFold, mActivity.getBounds());
+
+ }
+
+ private void setFoldablePosture(boolean isHalfFolded, boolean isTabletop) {
+ final DisplayRotation r = mActivity.mDisplayContent.getDisplayRotation();
+ doReturn(isHalfFolded).when(r).isDisplaySeparatingHinge();
+ doReturn(false).when(r).isDeviceInPosture(any(FoldState.class), anyBoolean());
+ if (isHalfFolded) {
+ doReturn(true).when(r).isDeviceInPosture(FoldState.HALF_FOLDED, isTabletop);
+ }
+ mActivity.recomputeConfiguration();
+ }
+
+ @Test
public void testUpdateResolvedBoundsPosition_alignToTop() {
final int notchHeight = 100;
final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2800)