diff options
212 files changed, 6690 insertions, 1997 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index c0e89d2c4a05..14bf5320a438 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -797,6 +797,7 @@ package android.content.pm { field public static final long OVERRIDE_MIN_ASPECT_RATIO_MEDIUM = 180326845L; // 0xabf91bdL field public static final float OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE = 1.5f; field public static final long OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY = 203647190L; // 0xc2368d6L + field public static final long OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS = 237531167L; // 0xe28701fL field public static final int RESIZE_MODE_RESIZEABLE = 2; // 0x2 } @@ -2917,7 +2918,9 @@ package android.view { } @UiThread public class View implements android.view.accessibility.AccessibilityEventSource android.graphics.drawable.Drawable.Callback android.view.KeyEvent.Callback { + method public void getBoundsOnScreen(@NonNull android.graphics.Rect, boolean); method public android.view.View getTooltipView(); + method public void getWindowDisplayFrame(@NonNull android.graphics.Rect); method public boolean isAutofilled(); method public static boolean isDefaultFocusHighlightEnabled(); method public boolean isDefaultFocusHighlightNeeded(android.graphics.drawable.Drawable, android.graphics.drawable.Drawable); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 0fd80c5698a9..7f48e39a9a79 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -6965,8 +6965,10 @@ public class Notification implements Parcelable /** * Returns whether an app can colorize due to the android.permission.USE_COLORIZED_NOTIFICATIONS * permission. The permission is checked when a notification is enqueued. + * + * @hide */ - private boolean hasColorizedPermission() { + public boolean hasColorizedPermission() { return (flags & Notification.FLAG_CAN_COLORIZE) != 0; } diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index bbe99f59fc21..226278cbe44d 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1104,6 +1104,34 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { 264301586L; // buganizer id /** + * This change id forces the packages it is applied to sandbox {@link android.view.View} API to + * an activity bounds for: + * + * <p>{@link android.view.View#getLocationOnScreen}, + * {@link android.view.View#getWindowVisibleDisplayFrame}, + * {@link android.view.View}#getWindowDisplayFrame, + * {@link android.view.View}#getBoundsOnScreen. + * + * <p>For {@link android.view.View#getWindowVisibleDisplayFrame} and + * {@link android.view.View}#getWindowDisplayFrame this sandboxing is happening indirectly + * through + * {@link android.view.ViewRootImpl}#getWindowVisibleDisplayFrame, + * {@link android.view.ViewRootImpl}#getDisplayFrame respectively. + * + * <p>Some applications assume that they occupy the whole screen and therefore use the display + * coordinates in their calculations as if an activity is positioned in the top-left corner of + * the screen, with left coordinate equal to 0. This may not be the case of applications in + * multi-window and in letterbox modes. This can lead to shifted or out of bounds UI elements in + * case the activity is Letterboxed or is in multi-window mode. + * @hide + */ + @ChangeId + @Overridable + @Disabled + @TestApi + public static final long OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS = 237531167L; // buganizer id + + /** * This change id is the gatekeeper for all treatments that force a given min aspect ratio. * Enabling this change will allow the following min aspect ratio treatments to be applied: * OVERRIDE_MIN_ASPECT_RATIO_MEDIUM diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java index a3892238f1e6..a2fa1392b079 100644 --- a/core/java/android/service/dreams/DreamActivity.java +++ b/core/java/android/service/dreams/DreamActivity.java @@ -58,13 +58,11 @@ public class DreamActivity extends Activity { setTitle(title); } - final Object callback = getIntent().getExtras().getBinder(EXTRA_CALLBACK); - if (callback instanceof DreamService.DreamActivityCallbacks) { - mCallback = (DreamService.DreamActivityCallbacks) callback; + final Bundle extras = getIntent().getExtras(); + mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK); + + if (mCallback != null) { mCallback.onActivityCreated(this); - } else { - mCallback = null; - finishAndRemoveTask(); } } diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index d79ea8929047..224307297e76 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1409,6 +1409,10 @@ public class DreamService extends Service implements Window.Callback { // Request the DreamOverlay be told to dream with dream's window // parameters once the window has been attached. mDreamStartOverlayConsumer = overlay -> { + if (mWindow == null) { + Slog.d(TAG, "mWindow is null"); + return; + } try { overlay.startDream(mWindow.getAttributes(), mOverlayCallback, mDreamComponent.flattenToString(), diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index d7480e5037f4..543a22b0f4d0 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -8774,7 +8774,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ @UnsupportedAppUsage - public void getBoundsOnScreen(Rect outRect, boolean clipToParent) { + @TestApi + public void getBoundsOnScreen(@NonNull Rect outRect, boolean clipToParent) { if (mAttachInfo == null) { return; } @@ -8782,6 +8783,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, getBoundsToScreenInternal(position, clipToParent); outRect.set(Math.round(position.left), Math.round(position.top), Math.round(position.right), Math.round(position.bottom)); + // If "Sandboxing View Bounds APIs" override is enabled, applyViewBoundsSandboxingIfNeeded + // will sandbox outRect within window bounds. + mAttachInfo.mViewRootImpl.applyViewBoundsSandboxingIfNeeded(outRect); } /** @@ -15586,7 +15590,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @hide */ @UnsupportedAppUsage - public void getWindowDisplayFrame(Rect outRect) { + @TestApi + public void getWindowDisplayFrame(@NonNull Rect outRect) { if (mAttachInfo != null) { mAttachInfo.mViewRootImpl.getDisplayFrame(outRect); return; @@ -25786,6 +25791,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (info != null) { outLocation[0] += info.mWindowLeft; outLocation[1] += info.mWindowTop; + // If OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS override is enabled, + // applyViewLocationSandboxingIfNeeded sandboxes outLocation within window bounds. + info.mViewRootImpl.applyViewLocationSandboxingIfNeeded(outLocation); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index f9e84114a7da..f6211fd488d8 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -16,6 +16,7 @@ package android.view; +import static android.content.pm.ActivityInfo.OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS; import static android.graphics.HardwareRenderer.SYNC_CONTEXT_IS_STOPPED; import static android.graphics.HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND; import static android.os.IInputConstants.INVALID_INPUT_EVENT_ID; @@ -82,6 +83,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; @@ -94,12 +96,14 @@ import android.animation.LayoutTransition; import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Size; import android.annotation.UiContext; import android.app.ActivityManager; import android.app.ActivityThread; import android.app.ICompatCameraControlCallback; import android.app.ResourcesManager; import android.app.WindowConfiguration; +import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipData; import android.content.ClipDescription; @@ -871,6 +875,15 @@ public final class ViewRootImpl implements ViewParent, private boolean mRelayoutRequested; + /** + * Whether sandboxing of {@link android.view.View#getBoundsOnScreen}, + * {@link android.view.View#getLocationOnScreen(int[])}, + * {@link android.view.View#getWindowDisplayFrame} and + * {@link android.view.View#getWindowVisibleDisplayFrame} + * within Activity bounds is enabled for the current application. + */ + private final boolean mViewBoundsSandboxingEnabled; + private int mLastTransformHint = Integer.MIN_VALUE; /** @@ -958,6 +971,8 @@ public final class ViewRootImpl implements ViewParent, mHandwritingInitiator = new HandwritingInitiator(mViewConfiguration, mContext.getSystemService(InputMethodManager.class)); + mViewBoundsSandboxingEnabled = getViewBoundsSandboxingEnabled(); + String processorOverrideName = context.getResources().getString( R.string.config_inputEventCompatProcessorOverrideClassName); if (processorOverrideName.isEmpty()) { @@ -8426,6 +8441,9 @@ public final class ViewRootImpl implements ViewParent, */ void getDisplayFrame(Rect outFrame) { outFrame.set(mTmpFrames.displayFrame); + // Apply sandboxing here (in getter) due to possible layout updates on the client after + // mTmpFrames.displayFrame is received from the server. + applyViewBoundsSandboxingIfNeeded(outFrame); } /** @@ -8442,6 +8460,69 @@ public final class ViewRootImpl implements ViewParent, outFrame.top += insets.top; outFrame.right -= insets.right; outFrame.bottom -= insets.bottom; + // Apply sandboxing here (in getter) due to possible layout updates on the client after + // mTmpFrames.displayFrame is received from the server. + applyViewBoundsSandboxingIfNeeded(outFrame); + } + + /** + * Offset outRect to make it sandboxed within Window's bounds. + * + * <p>This is used by {@link android.view.View#getBoundsOnScreen}, + * {@link android.view.ViewRootImpl#getDisplayFrame} and + * {@link android.view.ViewRootImpl#getWindowVisibleDisplayFrame}, which are invoked by + * {@link android.view.View#getWindowDisplayFrame} and + * {@link android.view.View#getWindowVisibleDisplayFrame}, as well as + * {@link android.view.ViewDebug#captureLayers} for debugging. + */ + void applyViewBoundsSandboxingIfNeeded(final Rect inOutRect) { + if (mViewBoundsSandboxingEnabled) { + final Rect bounds = getConfiguration().windowConfiguration.getBounds(); + inOutRect.offset(-bounds.left, -bounds.top); + } + } + + /** + * Offset outLocation to make it sandboxed within Window's bounds. + * + * <p>This is used by {@link android.view.View#getLocationOnScreen(int[])} + */ + public void applyViewLocationSandboxingIfNeeded(@Size(2) int[] outLocation) { + if (mViewBoundsSandboxingEnabled) { + final Rect bounds = getConfiguration().windowConfiguration.getBounds(); + outLocation[0] -= bounds.left; + outLocation[1] -= bounds.top; + } + } + + private boolean getViewBoundsSandboxingEnabled() { + // System dialogs (e.g. ANR) can be created within System process, so handleBindApplication + // may be never called. This results into all app compat changes being enabled + // (see b/268007823) because AppCompatCallbacks.install() is never called with non-empty + // array. + // With ActivityThread.isSystem we verify that it is not the system process, + // then this CompatChange can take effect. + if (ActivityThread.isSystem() + || !CompatChanges.isChangeEnabled(OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS)) { + // It is a system process or OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS change-id is disabled. + return false; + } + + // OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS is enabled by the device manufacturer. + try { + final List<PackageManager.Property> properties = mContext.getPackageManager() + .queryApplicationProperty(PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS); + + final boolean isOptedOut = !properties.isEmpty() && !properties.get(0).getBoolean(); + if (isOptedOut) { + // PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS is disabled by the app devs. + return false; + } + } catch (RuntimeException e) { + // remote exception. + } + + return true; } /** diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 17df585e424a..a01c8328b37c 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -854,6 +854,42 @@ public interface WindowManager extends ViewManager { /** * Application level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that it needs to be opted-out from the + * compatibility treatment that sandboxes {@link android.view.View} API. + * + * <p>The treatment can be enabled by device manufacturers for applications which misuse + * {@link android.view.View} APIs by expecting that + * {@link android.view.View#getLocationOnScreen}, + * {@link android.view.View#getBoundsOnScreen}, + * {@link android.view.View#getWindowVisibleDisplayFrame}, + * {@link android.view.View#getWindowDisplayFrame} + * return coordinates as if an activity is positioned in the top-left corner of the screen, with + * left coordinate equal to 0. This may not be the case for applications in multi-window and in + * letterbox modes. + * + * <p>Setting this property to {@code false} informs the system that the application must be + * opted-out from the "Sandbox {@link android.view.View} API to Activity bounds" treatment even + * if the device manufacturer has opted the app into the treatment. + * + * <p>Not setting this property at all, or setting this property to {@code true} has no effect. + * + * <p><b>Syntax:</b> + * <pre> + * <application> + * <property + * android:name="android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS" + * android:value="false"/> + * </application> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS = + "android.window.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_BOUNDS_APIS"; + + /** + * Application level {@link android.content.pm.PackageManager.Property PackageManager * .Property} for an app to inform the system that the application can be opted-in or opted-out * from the compatibility treatment that enables sending a fake focus event for unfocused * resumed split screen activities. This is needed because some game engines wait to get diff --git a/core/java/com/android/internal/app/procstats/ProcessState.java b/core/java/com/android/internal/app/procstats/ProcessState.java index 72b9cd272d02..818a50366115 100644 --- a/core/java/com/android/internal/app/procstats/ProcessState.java +++ b/core/java/com/android/internal/app/procstats/ProcessState.java @@ -73,6 +73,7 @@ import com.android.internal.app.procstats.ProcessStats.TotalMemoryUseCollection; import java.io.PrintWriter; import java.util.Comparator; +import java.util.concurrent.TimeUnit; public final class ProcessState { private static final String TAG = "ProcessStats"; @@ -1542,6 +1543,75 @@ public final class ProcessState { proto.write(fieldId, procName); } + /** Dumps the duration of each state to statsEventOutput. */ + public void dumpStateDurationToStatsd( + int atomTag, ProcessStats processStats, StatsEventOutput statsEventOutput) { + long topMs = 0; + long fgsMs = 0; + long boundTopMs = 0; + long boundFgsMs = 0; + long importantForegroundMs = 0; + long cachedMs = 0; + long frozenMs = 0; + long otherMs = 0; + for (int i = 0, size = mDurations.getKeyCount(); i < size; i++) { + final int key = mDurations.getKeyAt(i); + final int type = SparseMappingTable.getIdFromKey(key); + int procStateIndex = type % STATE_COUNT; + long duration = mDurations.getValue(key); + switch (procStateIndex) { + case STATE_TOP: + topMs += duration; + break; + case STATE_BOUND_TOP_OR_FGS: + boundTopMs += duration; + break; + case STATE_FGS: + fgsMs += duration; + break; + case STATE_IMPORTANT_FOREGROUND: + case STATE_IMPORTANT_BACKGROUND: + importantForegroundMs += duration; + break; + case STATE_BACKUP: + case STATE_SERVICE: + case STATE_SERVICE_RESTARTING: + case STATE_RECEIVER: + case STATE_HEAVY_WEIGHT: + case STATE_HOME: + case STATE_LAST_ACTIVITY: + case STATE_PERSISTENT: + otherMs += duration; + break; + case STATE_CACHED_ACTIVITY: + case STATE_CACHED_ACTIVITY_CLIENT: + case STATE_CACHED_EMPTY: + cachedMs += duration; + break; + // TODO (b/261910877) Add support for tracking boundFgsMs and + // frozenMs. + } + } + statsEventOutput.write( + atomTag, + getUid(), + getName(), + (int) TimeUnit.MILLISECONDS.toSeconds(processStats.mTimePeriodStartUptime), + (int) TimeUnit.MILLISECONDS.toSeconds(processStats.mTimePeriodEndUptime), + (int) + TimeUnit.MILLISECONDS.toSeconds( + processStats.mTimePeriodEndUptime + - processStats.mTimePeriodStartUptime), + (int) TimeUnit.MILLISECONDS.toSeconds(topMs), + (int) TimeUnit.MILLISECONDS.toSeconds(fgsMs), + (int) TimeUnit.MILLISECONDS.toSeconds(boundTopMs), + (int) TimeUnit.MILLISECONDS.toSeconds(boundFgsMs), + (int) TimeUnit.MILLISECONDS.toSeconds(importantForegroundMs), + (int) TimeUnit.MILLISECONDS.toSeconds(cachedMs), + (int) TimeUnit.MILLISECONDS.toSeconds(frozenMs), + (int) TimeUnit.MILLISECONDS.toSeconds(otherMs)); + } + /** Similar to {@code #dumpDebug}, but with a reduced/aggregated subset of states. */ public void dumpAggregatedProtoForStatsd(ProtoOutputStream proto, long fieldId, String procName, int uid, long now, diff --git a/core/java/com/android/internal/app/procstats/ProcessStats.java b/core/java/com/android/internal/app/procstats/ProcessStats.java index d2b2f0a2b894..f3ed09a861e3 100644 --- a/core/java/com/android/internal/app/procstats/ProcessStats.java +++ b/core/java/com/android/internal/app/procstats/ProcessStats.java @@ -43,6 +43,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.app.ProcessMap; import com.android.internal.app.procstats.AssociationState.SourceKey; import com.android.internal.app.procstats.AssociationState.SourceState; +import com.android.internal.util.function.QuintConsumer; import dalvik.system.VMRuntime; @@ -56,6 +57,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -2389,6 +2392,79 @@ public final class ProcessStats implements Parcelable { } } + void forEachProcess(Consumer<ProcessState> consumer) { + final ArrayMap<String, SparseArray<ProcessState>> procMap = mProcesses.getMap(); + for (int ip = 0, size = procMap.size(); ip < size; ip++) { + final SparseArray<ProcessState> uids = procMap.valueAt(ip); + for (int iu = 0, uidsSize = uids.size(); iu < uidsSize; iu++) { + final ProcessState processState = uids.valueAt(iu); + consumer.accept(processState); + } + } + } + + void forEachAssociation( + QuintConsumer<AssociationState, Integer, String, SourceKey, SourceState> consumer) { + final ArrayMap<String, SparseArray<LongSparseArray<PackageState>>> pkgMap = + mPackages.getMap(); + for (int ip = 0, size = pkgMap.size(); ip < size; ip++) { + final SparseArray<LongSparseArray<PackageState>> uids = pkgMap.valueAt(ip); + for (int iu = 0, uidsSize = uids.size(); iu < uidsSize; iu++) { + final int uid = uids.keyAt(iu); + final LongSparseArray<PackageState> versions = uids.valueAt(iu); + for (int iv = 0, versionsSize = versions.size(); iv < versionsSize; iv++) { + final PackageState state = versions.valueAt(iv); + for (int iasc = 0, ascSize = state.mAssociations.size(); + iasc < ascSize; + iasc++) { + final String serviceName = state.mAssociations.keyAt(iasc); + final AssociationState asc = state.mAssociations.valueAt(iasc); + for (int is = 0, sourcesSize = asc.mSources.size(); + is < sourcesSize; + is++) { + final SourceState src = asc.mSources.valueAt(is); + final SourceKey key = asc.mSources.keyAt(is); + consumer.accept(asc, uid, serviceName, key, src); + } + } + } + } + } + } + + /** Dumps the stats of all processes to statsEventOutput. */ + public void dumpProcessState(int atomTag, StatsEventOutput statsEventOutput) { + forEachProcess( + (processState) -> { + if (processState.isMultiPackage() + && processState.getCommonProcess() != processState) { + return; + } + processState.dumpStateDurationToStatsd(atomTag, this, statsEventOutput); + }); + } + + /** Dumps all process association data to statsEventOutput. */ + public void dumpProcessAssociation(int atomTag, StatsEventOutput statsEventOutput) { + forEachAssociation( + (asc, serviceUid, serviceName, key, src) -> { + statsEventOutput.write( + atomTag, + key.mUid, + key.mProcess, + serviceUid, + serviceName, + (int) TimeUnit.MILLISECONDS.toSeconds(mTimePeriodStartUptime), + (int) TimeUnit.MILLISECONDS.toSeconds(mTimePeriodEndUptime), + (int) + TimeUnit.MILLISECONDS.toSeconds( + mTimePeriodEndUptime - mTimePeriodStartUptime), + (int) TimeUnit.MILLISECONDS.toSeconds(src.mDuration), + src.mActiveCount, + asc.getProcessName()); + }); + } + private void dumpProtoPreamble(ProtoOutputStream proto) { proto.write(ProcessStatsSectionProto.START_REALTIME_MS, mTimePeriodStartRealtime); proto.write(ProcessStatsSectionProto.END_REALTIME_MS, diff --git a/core/java/com/android/internal/app/procstats/StatsEventOutput.java b/core/java/com/android/internal/app/procstats/StatsEventOutput.java new file mode 100644 index 000000000000..b2e405475a4e --- /dev/null +++ b/core/java/com/android/internal/app/procstats/StatsEventOutput.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app.procstats; + +import android.util.StatsEvent; + +import com.android.internal.util.FrameworkStatsLog; + +import java.util.List; + +/** + * A simple wrapper of FrameworkStatsLog.buildStatsEvent. This allows unit tests to mock out the + * dependency. + */ +public class StatsEventOutput { + + List<StatsEvent> mOutput; + + public StatsEventOutput(List<StatsEvent> output) { + mOutput = output; + } + + /** Writes the data to the output. */ + public void write( + int atomTag, + int uid, + String processName, + int measurementStartUptimeSecs, + int measurementEndUptimeSecs, + int measurementDurationUptimeSecs, + int topSeconds, + int fgsSeconds, + int boundTopSeconds, + int boundFgsSeconds, + int importantForegroundSeconds, + int cachedSeconds, + int frozenSeconds, + int otherSeconds) { + mOutput.add( + FrameworkStatsLog.buildStatsEvent( + atomTag, + uid, + processName, + measurementStartUptimeSecs, + measurementEndUptimeSecs, + measurementDurationUptimeSecs, + topSeconds, + fgsSeconds, + boundTopSeconds, + boundFgsSeconds, + importantForegroundSeconds, + cachedSeconds, + frozenSeconds, + otherSeconds)); + } + + /** Writes the data to the output. */ + public void write( + int atomTag, + int clientUid, + String processName, + int serviceUid, + String serviceName, + int measurementStartUptimeSecs, + int measurementEndUptimeSecs, + int measurementDurationUptimeSecs, + int activeDurationUptimeSecs, + int activeCount, + String serviceProcessName) { + mOutput.add( + FrameworkStatsLog.buildStatsEvent( + atomTag, + clientUid, + processName, + serviceUid, + serviceName, + measurementStartUptimeSecs, + measurementEndUptimeSecs, + measurementDurationUptimeSecs, + activeDurationUptimeSecs, + activeCount, + serviceProcessName)); + } +} diff --git a/core/res/res/layout/transient_notification.xml b/core/res/res/layout/transient_notification.xml index 3259201f6e9b..8bedb897dc19 100644 --- a/core/res/res/layout/transient_notification.xml +++ b/core/res/res/layout/transient_notification.xml @@ -25,7 +25,7 @@ android:orientation="horizontal" android:gravity="center_vertical" android:maxWidth="@dimen/toast_width" - android:background="?android:attr/toastFrameBackground" + android:background="?android:attr/colorBackground" android:elevation="@dimen/toast_elevation" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" diff --git a/core/res/res/layout/transient_notification_with_icon.xml b/core/res/res/layout/transient_notification_with_icon.xml index e9b17df75333..0dfb3adc8364 100644 --- a/core/res/res/layout/transient_notification_with_icon.xml +++ b/core/res/res/layout/transient_notification_with_icon.xml @@ -22,7 +22,7 @@ android:orientation="horizontal" android:gravity="center_vertical" android:maxWidth="@dimen/toast_width" - android:background="?android:attr/toastFrameBackground" + android:background="?android:attr/colorBackground" android:elevation="@dimen/toast_elevation" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index dafa0ad7989f..993fcf877055 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6092,4 +6092,8 @@ <!-- Whether to show weather on the lock screen by default. --> <bool name="config_lockscreenWeatherEnabledByDefault">false</bool> + + <!-- Whether we should persist the brightness value in nits for the default display even if + the underlying display device changes. --> + <bool name="config_persistBrightnessNitsForDefaultDisplay">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 591ba5feeee9..c04c322e2d87 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2282,6 +2282,7 @@ <java-symbol type="bool" name="config_preventImeStartupUnlessTextEditor" /> <java-symbol type="array" name="config_nonPreemptibleInputMethods" /> <java-symbol type="bool" name="config_enhancedConfirmationModeEnabled" /> + <java-symbol type="bool" name="config_persistBrightnessNitsForDefaultDisplay" /> <java-symbol type="layout" name="resolver_list" /> <java-symbol type="id" name="resolver_list" /> diff --git a/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java b/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java new file mode 100644 index 000000000000..9b9a84b79da3 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/procstats/ProcessStatsTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app.procstats; + +import static com.android.internal.app.procstats.ProcessStats.STATE_TOP; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import androidx.test.filters.SmallTest; + +import com.android.internal.util.FrameworkStatsLog; + +import junit.framework.TestCase; + +import org.junit.Before; +import org.mockito.Mock; + +import java.util.concurrent.TimeUnit; + +/** Provides test cases for ProcessStats. */ +public class ProcessStatsTest extends TestCase { + + private static final String APP_1_PACKAGE_NAME = "com.android.testapp"; + private static final int APP_1_UID = 5001; + private static final long APP_1_VERSION = 10; + private static final String APP_1_PROCESS_NAME = "com.android.testapp.p"; + private static final String APP_1_SERVICE_NAME = "com.android.testapp.service"; + + private static final String APP_2_PACKAGE_NAME = "com.android.testapp2"; + private static final int APP_2_UID = 5002; + private static final long APP_2_VERSION = 30; + private static final String APP_2_PROCESS_NAME = "com.android.testapp2.p"; + + private static final long NOW_MS = 123000; + private static final int DURATION_SECS = 6; + + @Mock StatsEventOutput mStatsEventOutput; + + @Before + public void setUp() { + initMocks(this); + } + + @SmallTest + public void testDumpProcessState() throws Exception { + ProcessStats processStats = new ProcessStats(); + processStats.getProcessStateLocked( + APP_1_PACKAGE_NAME, APP_1_UID, APP_1_VERSION, APP_1_PROCESS_NAME); + processStats.getProcessStateLocked( + APP_2_PACKAGE_NAME, APP_2_UID, APP_2_VERSION, APP_2_PROCESS_NAME); + processStats.dumpProcessState(FrameworkStatsLog.PROCESS_STATE, mStatsEventOutput); + verify(mStatsEventOutput) + .write( + eq(FrameworkStatsLog.PROCESS_STATE), + eq(APP_1_UID), + eq(APP_1_PROCESS_NAME), + anyInt(), + anyInt(), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0)); + verify(mStatsEventOutput) + .write( + eq(FrameworkStatsLog.PROCESS_STATE), + eq(APP_2_UID), + eq(APP_2_PROCESS_NAME), + anyInt(), + anyInt(), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0)); + } + + @SmallTest + public void testNonZeroProcessStateDuration() throws Exception { + ProcessStats processStats = new ProcessStats(); + ProcessState processState = + processStats.getProcessStateLocked( + APP_1_PACKAGE_NAME, APP_1_UID, APP_1_VERSION, APP_1_PROCESS_NAME); + processState.setCombinedState(STATE_TOP, NOW_MS); + processState.commitStateTime(NOW_MS + TimeUnit.SECONDS.toMillis(DURATION_SECS)); + processStats.dumpProcessState(FrameworkStatsLog.PROCESS_STATE, mStatsEventOutput); + verify(mStatsEventOutput) + .write( + eq(FrameworkStatsLog.PROCESS_STATE), + eq(APP_1_UID), + eq(APP_1_PROCESS_NAME), + anyInt(), + anyInt(), + eq(0), + eq(DURATION_SECS), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0), + eq(0)); + } + + @SmallTest + public void testDumpProcessAssociation() throws Exception { + ProcessStats processStats = new ProcessStats(); + AssociationState associationState = + processStats.getAssociationStateLocked( + APP_1_PACKAGE_NAME, + APP_1_UID, + APP_1_VERSION, + APP_1_PROCESS_NAME, + APP_1_SERVICE_NAME); + AssociationState.SourceState sourceState = + associationState.startSource(APP_2_UID, APP_2_PROCESS_NAME, APP_2_PACKAGE_NAME); + sourceState.stop(); + processStats.dumpProcessAssociation( + FrameworkStatsLog.PROCESS_ASSOCIATION, mStatsEventOutput); + verify(mStatsEventOutput) + .write( + eq(FrameworkStatsLog.PROCESS_ASSOCIATION), + eq(APP_2_UID), + eq(APP_2_PROCESS_NAME), + eq(APP_1_UID), + eq(APP_1_SERVICE_NAME), + anyInt(), + anyInt(), + eq(0), + eq(0), + eq(0), + eq(APP_1_PROCESS_NAME)); + } +} diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index caa118a409f5..e1c9b3c121f8 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -517,6 +517,12 @@ applications that come with the platform <permission name="android.permission.BIND_WALLPAPER"/> </privapp-permissions> + <privapp-permissions package="com.android.wallpaper"> + <permission name="android.permission.SET_WALLPAPER_COMPONENT"/> + <permission name="android.permission.BIND_WALLPAPER"/> + <permission name="android.permission.CUSTOMIZE_SYSTEM_UI"/> + </privapp-permissions> + <privapp-permissions package="com.android.dynsystem"> <permission name="android.permission.REBOOT"/> <permission name="android.permission.MANAGE_DYNAMIC_SYSTEM"/> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 1cf819af7a24..3d7fb16bb846 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1051,6 +1051,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/WindowContainer.java" }, + "-1104347731": { + "message": "Setting requested orientation %s for %s", + "level": "VERBOSE", + "group": "WM_DEBUG_ORIENTATION", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "-1103716954": { "message": "Not removing %s due to exit animation", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index c08085ee8cd0..541c0f04b9b9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -135,6 +135,30 @@ public class BubbleController implements ConfigurationChangeListener { private static final boolean BUBBLE_BAR_ENABLED = SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); + + /** + * Common interface to send updates to bubble views. + */ + public interface BubbleViewCallback { + /** Called when the provided bubble should be removed. */ + void removeBubble(Bubble removedBubble); + /** Called when the provided bubble should be added. */ + void addBubble(Bubble addedBubble); + /** Called when the provided bubble should be updated. */ + void updateBubble(Bubble updatedBubble); + /** Called when the provided bubble should be selected. */ + void selectionChanged(BubbleViewProvider selectedBubble); + /** Called when the provided bubble's suppression state has changed. */ + void suppressionChanged(Bubble bubble, boolean isSuppressed); + /** Called when the expansion state of bubbles has changed. */ + void expansionChanged(boolean isExpanded); + /** + * Called when the order of the bubble list has changed. Depending on the expanded state + * the pointer might need to be updated. + */ + void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer); + } + private final Context mContext; private final BubblesImpl mImpl = new BubblesImpl(); private Bubbles.BubbleExpandListener mExpandListener; @@ -157,7 +181,6 @@ public class BubbleController implements ConfigurationChangeListener { // Used to post to main UI thread private final ShellExecutor mMainExecutor; private final Handler mMainHandler; - private final ShellExecutor mBackgroundExecutor; private BubbleLogger mLogger; @@ -1285,6 +1308,58 @@ public class BubbleController implements ConfigurationChangeListener { }); } + private final BubbleViewCallback mBubbleViewCallback = new BubbleViewCallback() { + @Override + public void removeBubble(Bubble removedBubble) { + if (mStackView != null) { + mStackView.removeBubble(removedBubble); + } + } + + @Override + public void addBubble(Bubble addedBubble) { + if (mStackView != null) { + mStackView.addBubble(addedBubble); + } + } + + @Override + public void updateBubble(Bubble updatedBubble) { + if (mStackView != null) { + mStackView.updateBubble(updatedBubble); + } + } + + @Override + public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) { + if (mStackView != null) { + mStackView.updateBubbleOrder(bubbleOrder, updatePointer); + } + } + + @Override + public void suppressionChanged(Bubble bubble, boolean isSuppressed) { + if (mStackView != null) { + mStackView.setBubbleSuppressed(bubble, isSuppressed); + } + } + + @Override + public void expansionChanged(boolean isExpanded) { + if (mStackView != null) { + mStackView.setExpanded(isExpanded); + } + } + + @Override + public void selectionChanged(BubbleViewProvider selectedBubble) { + if (mStackView != null) { + mStackView.setSelectedBubble(selectedBubble); + } + + } + }; + @SuppressWarnings("FieldCanBeLocal") private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() { @@ -1307,7 +1382,8 @@ public class BubbleController implements ConfigurationChangeListener { // Lazy load overflow bubbles from disk loadOverflowBubblesFromDisk(); - mStackView.updateOverflowButtonDot(); + // If bubbles in the overflow have a dot, make sure the overflow shows a dot + updateOverflowButtonDot(); // Update bubbles in overflow. if (mOverflowListener != null) { @@ -1322,9 +1398,7 @@ public class BubbleController implements ConfigurationChangeListener { final Bubble bubble = removed.first; @Bubbles.DismissReason final int reason = removed.second; - if (mStackView != null) { - mStackView.removeBubble(bubble); - } + mBubbleViewCallback.removeBubble(bubble); // Leave the notification in place if we're dismissing due to user switching, or // because DND is suppressing the bubble. In both of those cases, we need to be able @@ -1354,49 +1428,47 @@ public class BubbleController implements ConfigurationChangeListener { } mDataRepository.removeBubbles(mCurrentUserId, bubblesToBeRemovedFromRepository); - if (update.addedBubble != null && mStackView != null) { + if (update.addedBubble != null) { mDataRepository.addBubble(mCurrentUserId, update.addedBubble); - mStackView.addBubble(update.addedBubble); + mBubbleViewCallback.addBubble(update.addedBubble); } - if (update.updatedBubble != null && mStackView != null) { - mStackView.updateBubble(update.updatedBubble); + if (update.updatedBubble != null) { + mBubbleViewCallback.updateBubble(update.updatedBubble); } - if (update.suppressedBubble != null && mStackView != null) { - mStackView.setBubbleSuppressed(update.suppressedBubble, true); + if (update.suppressedBubble != null) { + mBubbleViewCallback.suppressionChanged(update.suppressedBubble, true); } - if (update.unsuppressedBubble != null && mStackView != null) { - mStackView.setBubbleSuppressed(update.unsuppressedBubble, false); + if (update.unsuppressedBubble != null) { + mBubbleViewCallback.suppressionChanged(update.unsuppressedBubble, false); } boolean collapseStack = update.expandedChanged && !update.expanded; // At this point, the correct bubbles are inflated in the stack. // Make sure the order in bubble data is reflected in bubble row. - if (update.orderChanged && mStackView != null) { + if (update.orderChanged) { mDataRepository.addBubbles(mCurrentUserId, update.bubbles); // if the stack is going to be collapsed, do not update pointer position // after reordering - mStackView.updateBubbleOrder(update.bubbles, !collapseStack); + mBubbleViewCallback.bubbleOrderChanged(update.bubbles, !collapseStack); } if (collapseStack) { - mStackView.setExpanded(false); + mBubbleViewCallback.expansionChanged(/* expanded= */ false); mSysuiProxy.requestNotificationShadeTopUi(false, TAG); } - if (update.selectionChanged && mStackView != null) { - mStackView.setSelectedBubble(update.selectedBubble); + if (update.selectionChanged) { + mBubbleViewCallback.selectionChanged(update.selectedBubble); } // Expanding? Apply this last. if (update.expandedChanged && update.expanded) { - if (mStackView != null) { - mStackView.setExpanded(true); - mSysuiProxy.requestNotificationShadeTopUi(true, TAG); - } + mBubbleViewCallback.expansionChanged(/* expanded= */ true); + mSysuiProxy.requestNotificationShadeTopUi(true, TAG); } mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate"); @@ -1407,6 +1479,19 @@ public class BubbleController implements ConfigurationChangeListener { } }; + private void updateOverflowButtonDot() { + BubbleOverflow overflow = mBubbleData.getOverflow(); + if (overflow == null) return; + + for (Bubble b : mBubbleData.getOverflowBubbles()) { + if (b.showDot()) { + overflow.setShowDot(true); + return; + } + } + overflow.setShowDot(false); + } + private boolean handleDismissalInterception(BubbleEntry entry, @Nullable List<BubbleEntry> children, IntConsumer removeCallback) { if (isSummaryOfBubbles(entry)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 15f8eaca0833..5ecbd6b596b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -1360,16 +1360,6 @@ public class BubbleStackView extends FrameLayout updateOverflowVisibility(); } - void updateOverflowButtonDot() { - for (Bubble b : mBubbleData.getOverflowBubbles()) { - if (b.showDot()) { - mBubbleOverflow.setShowDot(true); - return; - } - } - mBubbleOverflow.setShowDot(false); - } - /** * Handle theme changes. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java index 22587f4c6456..8b4ac1a8dc79 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java @@ -39,6 +39,9 @@ import java.util.List; * * Note that most of the implementation here inherits from * {@link com.android.systemui.statusbar.policy.DevicePostureController}. + * + * Use the {@link TabletopModeController} if you are interested in tabletop mode change only, + * which is more common. */ public class DevicePostureController { @IntDef(prefix = {"DEVICE_POSTURE_"}, value = { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java new file mode 100644 index 000000000000..bf226283ae54 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_UNKNOWN; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_FOLDABLE; + +import android.annotation.NonNull; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Configuration; +import android.util.ArraySet; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * Wrapper class to track the tabletop (aka. flex) mode change on Fold-ables. + * See also <a + * href="https://developer.android.com/guide/topics/large-screens/learn-about-foldables + * #foldable_postures">Foldable states and postures</a> for reference. + * + * Use the {@link DevicePostureController} for more detailed posture changes. + */ +public class TabletopModeController implements + DevicePostureController.OnDevicePostureChangedListener, + DisplayController.OnDisplaysChangedListener { + private static final long TABLETOP_MODE_DELAY_MILLIS = 1_000; + + private final Context mContext; + + private final DevicePostureController mDevicePostureController; + + private final DisplayController mDisplayController; + + private final ShellExecutor mMainExecutor; + + private final Set<Integer> mTabletopModeRotations = new ArraySet<>(); + + private final List<OnTabletopModeChangedListener> mListeners = new ArrayList<>(); + + @VisibleForTesting + final Runnable mOnEnterTabletopModeCallback = () -> { + if (isInTabletopMode()) { + // We are still in tabletop mode, go ahead. + mayBroadcastOnTabletopModeChange(true /* isInTabletopMode */); + } + }; + + @DevicePostureController.DevicePostureInt + private int mDevicePosture = DEVICE_POSTURE_UNKNOWN; + + @Surface.Rotation + private int mDisplayRotation = WindowConfiguration.ROTATION_UNDEFINED; + + /** + * Track the last callback value for {@link OnTabletopModeChangedListener}. + * This is to avoid duplicated {@code false} callback to {@link #mListeners}. + */ + private Boolean mLastIsInTabletopModeForCallback; + + public TabletopModeController(Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mDevicePostureController = postureController; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + shellInit.addInitCallback(this::onInit, this); + } + + @VisibleForTesting + void onInit() { + mDevicePostureController.registerOnDevicePostureChangedListener(this); + mDisplayController.addDisplayWindowListener(this); + // Aligns with what's in {@link com.android.server.wm.DisplayRotation}. + final int[] deviceTabletopRotations = mContext.getResources().getIntArray( + com.android.internal.R.array.config_deviceTabletopRotations); + if (deviceTabletopRotations == null || deviceTabletopRotations.length == 0) { + ProtoLog.e(WM_SHELL_FOLDABLE, + "No valid config_deviceTabletopRotations, can not tell" + + " tabletop mode in WMShell"); + return; + } + for (int angle : deviceTabletopRotations) { + switch (angle) { + case 0: + mTabletopModeRotations.add(Surface.ROTATION_0); + break; + case 90: + mTabletopModeRotations.add(Surface.ROTATION_90); + break; + case 180: + mTabletopModeRotations.add(Surface.ROTATION_180); + break; + case 270: + mTabletopModeRotations.add(Surface.ROTATION_270); + break; + default: + ProtoLog.e(WM_SHELL_FOLDABLE, + "Invalid surface rotation angle in " + + "config_deviceTabletopRotations: %d", + angle); + break; + } + } + } + + /** Register {@link OnTabletopModeChangedListener} to listen for tabletop mode change. */ + public void registerOnTabletopModeChangedListener( + @NonNull OnTabletopModeChangedListener listener) { + if (listener == null || mListeners.contains(listener)) return; + mListeners.add(listener); + listener.onTabletopModeChanged(isInTabletopMode()); + } + + /** Unregister {@link OnTabletopModeChangedListener} for tabletop mode change. */ + public void unregisterOnTabletopModeChangedListener( + @NonNull OnTabletopModeChangedListener listener) { + mListeners.remove(listener); + } + + @Override + public void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) { + if (mDevicePosture != posture) { + onDevicePostureOrDisplayRotationChanged(posture, mDisplayRotation); + } + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + final int newDisplayRotation = newConfig.windowConfiguration.getDisplayRotation(); + if (displayId == DEFAULT_DISPLAY && newDisplayRotation != mDisplayRotation) { + onDevicePostureOrDisplayRotationChanged(mDevicePosture, newDisplayRotation); + } + } + + private void onDevicePostureOrDisplayRotationChanged( + @DevicePostureController.DevicePostureInt int newPosture, + @Surface.Rotation int newDisplayRotation) { + final boolean wasInTabletopMode = isInTabletopMode(); + mDevicePosture = newPosture; + mDisplayRotation = newDisplayRotation; + final boolean couldBeInTabletopMode = isInTabletopMode(); + mMainExecutor.removeCallbacks(mOnEnterTabletopModeCallback); + if (!wasInTabletopMode && couldBeInTabletopMode) { + // May enter tabletop mode, but we need to wait for additional time since this + // could be an intermediate state. + mMainExecutor.executeDelayed(mOnEnterTabletopModeCallback, TABLETOP_MODE_DELAY_MILLIS); + } else { + // Cancel entering tabletop mode if any condition's changed. + mayBroadcastOnTabletopModeChange(false /* isInTabletopMode */); + } + } + + private boolean isHalfOpened(@DevicePostureController.DevicePostureInt int posture) { + return posture == DEVICE_POSTURE_HALF_OPENED; + } + + private boolean isInTabletopMode() { + return isHalfOpened(mDevicePosture) && mTabletopModeRotations.contains(mDisplayRotation); + } + + private void mayBroadcastOnTabletopModeChange(boolean isInTabletopMode) { + if (mLastIsInTabletopModeForCallback == null + || mLastIsInTabletopModeForCallback != isInTabletopMode) { + mListeners.forEach(l -> l.onTabletopModeChanged(isInTabletopMode)); + mLastIsInTabletopModeForCallback = isInTabletopMode; + } + } + + /** + * Listener interface for tabletop mode change. + */ + public interface OnTabletopModeChangedListener { + /** + * Callback when tabletop mode changes. Expect duplicated callbacks with {@code false}. + * @param isInTabletopMode {@code true} if enters tabletop mode, {@code false} otherwise. + */ + void onTabletopModeChanged(boolean isInTabletopMode); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index abb357c5b653..bdf0ac2ed30c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -247,11 +247,11 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Stops showing resizing hint. */ public void onResized(SurfaceControl.Transaction t, Runnable animFinishedCallback) { - if (mScreenshot != null) { - if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { - mScreenshotAnimator.cancel(); - } + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + if (mScreenshot != null) { t.setPosition(mScreenshot, mOffsetX, mOffsetY); final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); @@ -321,6 +321,10 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Screenshot host leash and attach on it if meet some conditions */ public void screenshotIfNeeded(SurfaceControl.Transaction t) { if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + mTempRect.set(mOldBounds); mTempRect.offsetTo(0, 0); mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect, @@ -333,6 +337,10 @@ public class SplitDecorManager extends WindowlessWindowManager { if (screenshot == null || !screenshot.isValid()) return; if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) { + if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) { + mScreenshotAnimator.cancel(); + } + mScreenshot = screenshot; t.reparent(screenshot, mHostLeash); t.setLayer(screenshot, Integer.MAX_VALUE - 1); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 72dc771ee08c..ef21c7e9ec0c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -51,6 +51,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TabletopModeController; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellAnimationThread; @@ -171,6 +172,18 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides + static TabletopModeController provideTabletopModeController( + Context context, + ShellInit shellInit, + DevicePostureController postureController, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + return new TabletopModeController( + context, shellInit, postureController, displayController, mainExecutor); + } + + @WMSingleton + @Provides static DragAndDropController provideDragAndDropController(Context context, ShellInit shellInit, ShellController shellController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java index ac13f96585b6..f9e0ca53b32d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java @@ -247,6 +247,11 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { mLaunchRootTask = taskInfo; } + if (mHomeTask != null && mHomeTask.taskId == taskInfo.taskId + && !taskInfo.equals(mHomeTask)) { + mHomeTask = taskInfo; + } + super.onTaskInfoChanged(taskInfo); } @@ -364,6 +369,7 @@ public class KidsModeTaskOrganizer extends ShellTaskOrganizer { final WindowContainerTransaction wct = getWindowContainerTransaction(); final Rect taskBounds = calculateBounds(); wct.setBounds(mLaunchRootTask.token, taskBounds); + wct.setBounds(mHomeTask.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight)); mSyncQueue.queue(wct); final SurfaceControl finalLeash = mLaunchRootLeash; mSyncQueue.runInSync(t -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl index 2624ee536b58..d961d8658b98 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl @@ -70,4 +70,9 @@ interface IPip { * Sets the next pip animation type to be the alpha animation. */ oneway void setPipAnimationTypeToAlpha() = 5; + + /** + * Sets the height and visibility of the Launcher keep clear area. + */ + oneway void setLauncherKeepClearAreaHeight(boolean visible, int height) = 6; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index e9d257139779..eb336d56b62c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1179,6 +1179,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** + * Directly update the animator bounds. + */ + public void updateAnimatorBounds(Rect bounds) { + final PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getCurrentAnimator(); + if (animator != null && animator.isRunning()) { + if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { + animator.updateEndValue(bounds); + } + animator.setDestinationBounds(bounds); + } + } + + /** * Handles all changes to the PictureInPictureParams. */ protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java index c6b5ce93fd35..db6138a0891f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java @@ -93,6 +93,11 @@ public class PipTransitionState { return hasEnteredPip(mState); } + /** Returns true if activity is currently entering PiP mode. */ + public boolean isEnteringPip() { + return isEnteringPip(mState); + } + public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) { mInSwipePipToHomeTransition = inSwipePipToHomeTransition; } @@ -130,6 +135,11 @@ public class PipTransitionState { return state == ENTERED_PIP; } + /** Returns true if activity is currently entering PiP mode. */ + public static boolean isEnteringPip(@TransitionState int state) { + return state == ENTERING_PIP; + } + public interface OnPipTransitionStateChangedListener { void onPipTransitionStateChanged(@TransitionState int oldState, @TransitionState int newState); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index fa3efeb51bd0..0d5f1432d204 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -101,6 +101,7 @@ import com.android.wm.shell.sysui.UserChangeListener; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -181,14 +182,20 @@ public class PipController implements PipTransitionController.PipTransitionCallb // early bail out if the keep clear areas feature is disabled return; } - // only move if already in pip, other transitions account for keep clear areas - if (mPipTransitionState.hasEnteredPip()) { + // only move if we're in PiP or transitioning into PiP + if (!mPipTransitionState.shouldBlockResizeRequest()) { Rect destBounds = mPipKeepClearAlgorithm.adjust(mPipBoundsState, mPipBoundsAlgorithm); // only move if the bounds are actually different if (destBounds != mPipBoundsState.getBounds()) { - mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, - mEnterAnimationDuration, null); + if (mPipTransitionState.hasEnteredPip()) { + // if already in PiP, schedule separate animation + mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, + mEnterAnimationDuration, null); + } else if (mPipTransitionState.isEnteringPip()) { + // while entering PiP we just need to update animator bounds + mPipTaskOrganizer.updateAnimatorBounds(destBounds); + } } } } @@ -874,6 +881,21 @@ public class PipController implements PipTransitionController.PipTransitionCallb } } + private void setLauncherKeepClearAreaHeight(boolean visible, int height) { + if (visible) { + Rect rect = new Rect( + 0, mPipBoundsState.getDisplayBounds().bottom - height, + mPipBoundsState.getDisplayBounds().right, + mPipBoundsState.getDisplayBounds().bottom); + Set<Rect> restrictedKeepClearAreas = new HashSet<>( + mPipBoundsState.getRestrictedKeepClearAreas()); + restrictedKeepClearAreas.add(rect); + mPipBoundsState.setKeepClearAreas(restrictedKeepClearAreas, + mPipBoundsState.getUnrestrictedKeepClearAreas()); + updatePipPositionForKeepClearAreas(); + } + } + private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { mOnIsInPipStateChangedListener = callback; if (mOnIsInPipStateChangedListener != null) { @@ -1237,6 +1259,14 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override + public void setLauncherKeepClearAreaHeight(boolean visible, int height) { + executeRemoteCallWithTaskPermission(mController, "setLauncherKeepClearAreaHeight", + (controller) -> { + controller.setLauncherKeepClearAreaHeight(visible, height); + }); + } + + @Override public void setPipAnimationListener(IPipAnimationListener listener) { executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener", (controller) -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 75f9a4c33af9..c9b3a1af6507 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -50,6 +50,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, + Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index c0e48105ffbe..71ee690146f9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -207,6 +207,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private boolean mIsDividerRemoteAnimating; private boolean mIsDropEntering; private boolean mIsExiting; + private boolean mIsRootTranslucent; private DefaultMixedHandler mMixedHandler; private final Toast mSplitUnsupportedToast; @@ -422,6 +423,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + if (!isSplitActive()) { + // prevent the fling divider to center transitioni if split screen didn't active. + mIsDropEntering = true; + } + setSideStagePosition(sideStagePosition, wct); final WindowContainerTransaction evictWct = new WindowContainerTransaction(); targetStage.evictAllChildren(evictWct); @@ -436,28 +442,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // reparent the task to an invisible split root will make the activity invisible. Reorder // the root task to front to make the entering transition from pip to split smooth. wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, true); wct.reorder(targetStage.mRootTaskInfo.token, true); - wct.setForceTranslucent(targetStage.mRootTaskInfo.token, true); - // prevent the fling divider to center transition - mIsDropEntering = true; - targetStage.addTask(task, wct); - if (ENABLE_SHELL_TRANSITIONS) { - prepareEnterSplitScreen(wct); - mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, - null, this, null /* consumedCallback */, (finishWct, finishT) -> { - if (!evictWct.isEmpty()) { - finishWct.merge(evictWct, true); - } - } /* finishedCallback */); - } else { - if (!evictWct.isEmpty()) { - wct.merge(evictWct, true /* transfer */); - } - mTaskOrganizer.applyTransaction(wct); + if (!evictWct.isEmpty()) { + wct.merge(evictWct, true /* transfer */); } + mTaskOrganizer.applyTransaction(wct); return true; } @@ -716,7 +707,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setDivideRatio(splitRatio); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); // Make sure the launch options will put tasks in the corresponding split roots mainOptions = mainOptions != null ? mainOptions : new Bundle(); @@ -923,7 +914,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); // TODO(b/268008375): Merge APIs to start a split pair into one. if (mainTaskId != INVALID_TASK_ID) { @@ -1344,7 +1335,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.removeAllTasks(wct, false /* toTop */); mMainStage.deactivate(wct, false /* toTop */); wct.reorder(mRootTaskInfo.token, false /* onTop */); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); onTransitionAnimationComplete(); } else { @@ -1376,7 +1367,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */); mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */); finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); - finishedWCT.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); mSyncQueue.queue(finishedWCT); mSyncQueue.runInSync(at -> { @@ -1488,7 +1479,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); } void finishEnterSplitScreen(SurfaceControl.Transaction t) { @@ -1692,6 +1683,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRootTaskInfo = null; mRootTaskLeash = null; + mIsRootTranslucent = false; } @@ -1710,7 +1702,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Make the stages adjacent to each other so they occlude what's behind them. wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); mSplitLayout.getInvisibleBounds(mTempRect1); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); mSyncQueue.queue(wct); @@ -1734,7 +1726,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.evictOtherChildren(wct, taskId); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { @@ -1758,6 +1750,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, mSplitLayout); } + private void setRootForceTranslucent(boolean translucent, WindowContainerTransaction wct) { + if (mIsRootTranslucent == translucent) return; + + mIsRootTranslucent = translucent; + wct.setForceTranslucent(mRootTaskInfo.token, translucent); + } + private void onStageVisibilityChanged(StageListenerImpl stageListener) { // If split didn't active, just ignore this callback because we should already did these // on #applyExitSplitScreen. @@ -1784,10 +1783,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Split entering background. wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, true /* setReparentLeafTaskIfRelaunch */); - wct.setForceTranslucent(mRootTaskInfo.token, true); + setRootForceTranslucent(true, wct); } else { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* setReparentLeafTaskIfRelaunch */); + setRootForceTranslucent(false, wct); } mSyncQueue.queue(wct); @@ -1919,7 +1919,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.activate(wct, true /* includingTopTask */); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); + setRootForceTranslucent(false, wct); } mSyncQueue.queue(wct); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 9224b3cbd798..6b7ca421f5ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -193,6 +193,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final DragDetector mDragDetector; private int mDragPointerId = -1; + private boolean mIsDragging; private CaptionTouchEventListener( RunningTaskInfo taskInfo, @@ -223,19 +224,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (v.getId() != R.id.caption) { return false; } - mDragDetector.onMotionEvent(e); - - if (e.getAction() != MotionEvent.ACTION_DOWN) { - return false; - } - final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - if (taskInfo.isFocused) { - return false; + if (e.getAction() == MotionEvent.ACTION_DOWN) { + final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + if (!taskInfo.isFocused) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reorder(mTaskToken, true /* onTop */); + mSyncQueue.queue(wct); + } } - final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.reorder(mTaskToken, true /* onTop */); - mSyncQueue.queue(wct); - return true; + return mDragDetector.onMotionEvent(e); } /** @@ -253,20 +250,24 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mDragPointerId = e.getPointerId(0); mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); - break; + mIsDragging = false; + return false; } case MotionEvent.ACTION_MOVE: { int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - break; + mIsDragging = true; + return true; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningEnd( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - break; + final boolean wasDragging = mIsDragging; + mIsDragging = false; + return wasDragging; } } return true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index c517f7be330b..dee5f8fa74d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -223,6 +223,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final DragPositioningCallback mDragPositioningCallback; private final DragDetector mDragDetector; + private boolean mIsDragging; private int mDragPointerId = -1; private DesktopModeTouchEventListener( @@ -273,23 +274,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) { return false; } - switch (e.getAction()) { - case MotionEvent.ACTION_DOWN: - mDragDetector.onMotionEvent(e); - final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - if (taskInfo.isFocused) { - return mDragDetector.isDragEvent(); - } - return false; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - boolean res = mDragDetector.isDragEvent(); - mDragDetector.onMotionEvent(e); - return res; - default: - mDragDetector.onMotionEvent(e); - return mDragDetector.isDragEvent(); - } + return mDragDetector.onMotionEvent(e); } /** @@ -313,13 +298,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragPointerId = e.getPointerId(0); mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); - break; + mIsDragging = false; + return false; } case MotionEvent.ACTION_MOVE: { final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); - break; + mIsDragging = true; + return true; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { @@ -336,7 +323,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { c -> c.moveToFullscreen(taskInfo)); } } - break; + final boolean wasDragging = mIsDragging; + mIsDragging = false; + return wasDragging; } } return true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java index cf1850b92373..65b5a7a17afe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -56,10 +56,15 @@ class DragDetector { * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed */ boolean onMotionEvent(MotionEvent ev) { + final boolean isTouchScreen = + (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + if (!isTouchScreen) { + // Only touches generate noisy moves, so mouse/trackpad events don't need to filtered + // to take the slop threshold into consideration. + return mEventHandler.handleMotionEvent(ev); + } switch (ev.getActionMasked()) { case ACTION_DOWN: { - // Only touch screens generate noisy moves. - mIsDragEvent = (ev.getSource() & SOURCE_TOUCHSCREEN) != SOURCE_TOUCHSCREEN; mDragPointerId = ev.getPointerId(0); float rawX = ev.getRawX(0); float rawY = ev.getRawY(0); @@ -72,8 +77,12 @@ class DragDetector { int dragPointerIndex = ev.findPointerIndex(mDragPointerId); float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x; float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y; + // Touches generate noisy moves, so only once the move is past the touch + // slop threshold should it be considered a drag. mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop; } + // The event handler should only be notified about 'move' events if a drag has been + // detected. if (mIsDragEvent) { return mEventHandler.handleMotionEvent(ev); } else { @@ -94,10 +103,6 @@ class DragDetector { mTouchSlop = touchSlop; } - boolean isDragEvent() { - return mIsDragEvent; - } - private void resetState() { mIsDragEvent = false; mInputDownPoint.set(0, 0); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index a3d364a0068e..0bce3acecb3c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -40,6 +40,7 @@ class TaskPositioner implements DragPositioningCallback { private final DisplayController mDisplayController; private final WindowDecoration mWindowDecoration; + private final Rect mTempBounds = new Rect(); private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mRepositionStartPoint = new PointF(); private final Rect mRepositionTaskBounds = new Rect(); @@ -117,17 +118,32 @@ class TaskPositioner implements DragPositioningCallback { final float deltaX = x - mRepositionStartPoint.x; final float deltaY = y - mRepositionStartPoint.y; mRepositionTaskBounds.set(mTaskBoundsAtDragStart); + + final Rect stableBounds = mTempBounds; + // Make sure the new resizing destination in any direction falls within the stable bounds. + // If not, set the bounds back to the old location that was valid to avoid conflicts with + // some regions such as the gesture area. + mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId()) + .getStableBounds(stableBounds); if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { - mRepositionTaskBounds.left += deltaX; + final int candidateLeft = mRepositionTaskBounds.left + (int) deltaX; + mRepositionTaskBounds.left = (candidateLeft > stableBounds.left) + ? candidateLeft : oldLeft; } if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) { - mRepositionTaskBounds.right += deltaX; + final int candidateRight = mRepositionTaskBounds.right + (int) deltaX; + mRepositionTaskBounds.right = (candidateRight < stableBounds.right) + ? candidateRight : oldRight; } if ((mCtrlType & CTRL_TYPE_TOP) != 0) { - mRepositionTaskBounds.top += deltaY; + final int candidateTop = mRepositionTaskBounds.top + (int) deltaY; + mRepositionTaskBounds.top = (candidateTop > stableBounds.top) + ? candidateTop : oldTop; } if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) { - mRepositionTaskBounds.bottom += deltaY; + final int candidateBottom = mRepositionTaskBounds.bottom + (int) deltaY; + mRepositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) + ? candidateBottom : oldBottom; } if (mCtrlType == CTRL_TYPE_UNDEFINED) { mRepositionTaskBounds.offset((int) deltaX, (int) deltaY); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java new file mode 100644 index 000000000000..96d202ce3a85 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TabletopModeControllerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; +import static com.android.wm.shell.common.DevicePostureController.DEVICE_POSTURE_OPENED; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link TabletopModeController}. + */ +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class TabletopModeControllerTest extends ShellTestCase { + // It's considered tabletop mode if the display rotation angle matches what's in this array. + // It's defined as com.android.internal.R.array.config_deviceTabletopRotations on real devices. + private static final int[] TABLETOP_MODE_ROTATIONS = new int[] { + 90 /* Surface.ROTATION_90 */, + 270 /* Surface.ROTATION_270 */ + }; + + private TestShellExecutor mMainExecutor; + + private Configuration mConfiguration; + + private TabletopModeController mPipTabletopController; + + @Mock + private Context mContext; + + @Mock + private ShellInit mShellInit; + + @Mock + private Resources mResources; + + @Mock + private DevicePostureController mDevicePostureController; + + @Mock + private DisplayController mDisplayController; + + @Mock + private TabletopModeController.OnTabletopModeChangedListener mOnTabletopModeChangedListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mResources.getIntArray(com.android.internal.R.array.config_deviceTabletopRotations)) + .thenReturn(TABLETOP_MODE_ROTATIONS); + when(mContext.getResources()).thenReturn(mResources); + mMainExecutor = new TestShellExecutor(); + mConfiguration = new Configuration(); + mPipTabletopController = new TabletopModeController(mContext, mShellInit, + mDevicePostureController, mDisplayController, mMainExecutor); + mPipTabletopController.onInit(); + } + + @Test + public void instantiateController_addInitCallback() { + verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipTabletopController)); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopMode_callbackFalse() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(false); + } + + @Test + public void registerOnTabletopModeChangedListener_inTabletopMode_callbackTrue() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + + verify(mOnTabletopModeChangedListener, times(1)) + .onTabletopModeChanged(true); + } + + @Test + public void registerOnTabletopModeChangedListener_notInTabletopModeTwice_callbackOnce() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.registerOnTabletopModeChangedListener( + mOnTabletopModeChangedListener); + clearInvocations(mOnTabletopModeChangedListener); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + verifyZeroInteractions(mOnTabletopModeChangedListener); + } + + // Test cases starting from folded state (DEVICE_POSTURE_CLOSED) + @Test + public void foldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void foldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + // Test cases starting from unfolded state (DEVICE_POSTURE_OPENED) + @Test + public void unfoldedRotation90_halfOpen_scheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertTrue(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation0_halfOpen_noScheduleTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenUnfold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenFold_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_CLOSED); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } + + @Test + public void unfoldedRotation90_halfOpenThenRotate_cancelTabletopModeChange() { + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_90); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + mPipTabletopController.onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED); + mConfiguration.windowConfiguration.setDisplayRotation(Surface.ROTATION_0); + mPipTabletopController.onDisplayConfigurationChanged(DEFAULT_DISPLAY, mConfiguration); + + assertFalse(mMainExecutor.hasCallback(mPipTabletopController.mOnEnterTabletopModeCallback)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt index 8f66f4e7e47b..94c064bda763 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt @@ -5,13 +5,16 @@ import android.app.WindowConfiguration import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner +import android.view.Display import android.window.WindowContainerToken +import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING import androidx.test.filters.SmallTest import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED @@ -19,10 +22,11 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations /** @@ -51,6 +55,8 @@ class TaskPositionerTest : ShellTestCase() { private lateinit var mockDisplayController: DisplayController @Mock private lateinit var mockDisplayLayout: DisplayLayout + @Mock + private lateinit var mockDisplay: Display private lateinit var taskPositioner: TaskPositioner @@ -68,6 +74,9 @@ class TaskPositionerTest : ShellTestCase() { `when`(taskToken.asBinder()).thenReturn(taskBinder) `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + `when`(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = TASK_ID @@ -78,6 +87,8 @@ class TaskPositionerTest : ShellTestCase() { displayId = DISPLAY_ID configuration.windowConfiguration.bounds = STARTING_BOUNDS } + mockWindowDecoration.mDisplay = mockDisplay + `when`(mockDisplay.displayId).thenAnswer { DISPLAY_ID } } @Test @@ -451,6 +462,72 @@ class TaskPositionerTest : ShellTestCase() { }) } + fun testDragResize_toDisallowedBounds_freezesAtLimit() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, // Resize right-bottom corner + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat() + ) + + // Resize the task by 10px to the right and bottom, a valid destination + val newBounds = Rect( + STARTING_BOUNDS.left, + STARTING_BOUNDS.top, + STARTING_BOUNDS.right + 10, + STARTING_BOUNDS.bottom + 10) + taskPositioner.onDragPositioningMove( + newBounds.right.toFloat(), + newBounds.bottom.toFloat() + ) + + // Resize the task by another 10px to the right (allowed) and to just in the disallowed + // area of the Y coordinate. + val newBounds2 = Rect( + newBounds.left, + newBounds.top, + newBounds.right + 10, + DISALLOWED_RESIZE_AREA.top + ) + taskPositioner.onDragPositioningMove( + newBounds2.right.toFloat(), + newBounds2.bottom.toFloat() + ) + + taskPositioner.onDragPositioningEnd(newBounds2.right.toFloat(), newBounds2.bottom.toFloat()) + + // The first resize falls in the allowed area, verify there's a change for it. + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds(newBounds) + } + }) + // The second resize falls in the disallowed area, verify there's no change for it. + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds(newBounds2) + } + }) + // Instead, there should be a change for its allowed portion (the X movement) with the Y + // staying frozen in the last valid resize position. + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && change.ofBounds( + Rect( + newBounds2.left, + newBounds2.top, + newBounds2.right, + newBounds.bottom // Stayed at the first resize destination. + ) + ) + } + }) + } + + private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { + return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) && + bounds == configuration.windowConfiguration.bounds + } + companion object { private const val TASK_ID = 5 private const val MIN_WIDTH = 10 @@ -458,6 +535,19 @@ class TaskPositionerTest : ShellTestCase() { private const val DENSITY_DPI = 20 private const val DEFAULT_MIN = 40 private const val DISPLAY_ID = 1 + private const val NAVBAR_HEIGHT = 50 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val DISALLOWED_RESIZE_AREA = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom) + private val STABLE_BOUNDS = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT + ) } } diff --git a/media/java/android/media/projection/OWNERS b/media/java/android/media/projection/OWNERS index 9ca391013aa3..2273f816ac60 100644 --- a/media/java/android/media/projection/OWNERS +++ b/media/java/android/media/projection/OWNERS @@ -1,2 +1,4 @@ michaelwr@google.com santoscordon@google.com +chaviw@google.com +nmusgrave@google.com diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt index 290328894439..f0a82113c3a3 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt @@ -19,14 +19,15 @@ package com.android.systemui.animation import android.graphics.fonts.Font import android.graphics.fonts.FontVariationAxis import android.util.MathUtils +import android.util.MathUtils.abs +import java.lang.Float.max +import java.lang.Float.min private const val TAG_WGHT = "wght" private const val TAG_ITAL = "ital" -private const val FONT_WEIGHT_MAX = 1000f -private const val FONT_WEIGHT_MIN = 0f -private const val FONT_WEIGHT_ANIMATION_STEP = 10f private const val FONT_WEIGHT_DEFAULT_VALUE = 400f +private const val FONT_WEIGHT_ANIMATION_FRAME_COUNT = 100 private const val FONT_ITALIC_MAX = 1f private const val FONT_ITALIC_MIN = 0f @@ -118,14 +119,17 @@ class FontInterpolator { lerp(startAxes, endAxes) { tag, startValue, endValue -> when (tag) { // TODO: Good to parse 'fvar' table for retrieving default value. - TAG_WGHT -> - adjustWeight( + TAG_WGHT -> { + adaptiveAdjustWeight( MathUtils.lerp( startValue ?: FONT_WEIGHT_DEFAULT_VALUE, endValue ?: FONT_WEIGHT_DEFAULT_VALUE, progress - ) + ), + startValue ?: FONT_WEIGHT_DEFAULT_VALUE, + endValue ?: FONT_WEIGHT_DEFAULT_VALUE, ) + } TAG_ITAL -> adjustItalic( MathUtils.lerp( @@ -205,10 +209,14 @@ class FontInterpolator { return result } - // For the performance reasons, we animate weight with FONT_WEIGHT_ANIMATION_STEP. This helps + // For the performance reasons, we animate weight with adaptive step. This helps // Cache hit ratio in the Skia glyph cache. - private fun adjustWeight(value: Float) = - coerceInWithStep(value, FONT_WEIGHT_MIN, FONT_WEIGHT_MAX, FONT_WEIGHT_ANIMATION_STEP) + // The reason we don't use fix step is because the range of weight axis is not normalized, + // some are from 50 to 100, others are from 0 to 1000, so we cannot give a constant proper step + private fun adaptiveAdjustWeight(value: Float, start: Float, end: Float): Float { + val step = max(abs(end - start) / FONT_WEIGHT_ANIMATION_FRAME_COUNT, 1F) + return coerceInWithStep(value, min(start, end), max(start, end), step) + } // For the performance reasons, we animate italic with FONT_ITALIC_ANIMATION_STEP. This helps // Cache hit ratio in the Skia glyph cache. diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index a08b59829de2..7fe94d349c42 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -190,6 +190,7 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { * @param textSize an optional font size. * @param colors an optional colors array that must be the same size as numLines passed to * the TextInterpolator + * @param strokeWidth an optional paint stroke width * @param animate an optional boolean indicating true for showing style transition as animation, * false for immediate style transition. True by default. * @param duration an optional animation duration in milliseconds. This is ignored if animate is @@ -201,6 +202,7 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { weight: Int = -1, textSize: Float = -1f, color: Int? = null, + strokeWidth: Float = -1f, animate: Boolean = true, duration: Long = -1L, interpolator: TimeInterpolator? = null, @@ -254,6 +256,9 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) { if (color != null) { textInterpolator.targetPaint.color = color } + if (strokeWidth >= 0F) { + textInterpolator.targetPaint.strokeWidth = strokeWidth + } textInterpolator.onTargetPaintModified() if (animate) { diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt index 468a8b10bc01..3eb7fd84dc49 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt @@ -473,6 +473,7 @@ class TextInterpolator(layout: Layout) { // TODO(172943390): Add other interpolation or support custom interpolator. out.textSize = MathUtils.lerp(from.textSize, to.textSize, progress) out.color = ColorUtils.blendARGB(from.color, to.color, progress) + out.strokeWidth = MathUtils.lerp(from.strokeWidth, to.strokeWidth, progress) } // Shape the text and stores the result to out argument. diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt index f64ea4561906..459a38e9318c 100644 --- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt +++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/DemotingTestWithoutBugDetector.kt @@ -25,6 +25,7 @@ import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner +import java.util.regex.Pattern import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UElement @@ -36,22 +37,38 @@ class DemotingTestWithoutBugDetector : Detector(), SourceCodeScanner { override fun createUastHandler(context: JavaContext): UElementHandler { return object : UElementHandler() { override fun visitAnnotation(node: UAnnotation) { - if (node.qualifiedName !in DEMOTING_ANNOTATION) { - return + // Annotations having int bugId field + if (node.qualifiedName in DEMOTING_ANNOTATION_BUG_ID) { + val bugId = node.findAttributeValue("bugId")!!.evaluate() as Int + if (bugId <= 0) { + val location = context.getLocation(node) + val message = "Please attach a bug id to track demoted test" + context.report(ISSUE, node, location, message) + } } - val bugId = node.findAttributeValue("bugId")!!.evaluate() as Int - if (bugId <= 0) { - val location = context.getLocation(node) - val message = "Please attach a bug id to track demoted test" - context.report(ISSUE, node, location, message) + // @Ignore has a String field for reason + if (node.qualifiedName == DEMOTING_ANNOTATION_IGNORE) { + val reason = node.findAttributeValue("value")!!.evaluate() as String + val bugPattern = Pattern.compile("b/\\d+") + if (!bugPattern.matcher(reason).find()) { + val location = context.getLocation(node) + val message = "Please attach a bug (e.g. b/123) to track demoted test" + context.report(ISSUE, node, location, message) + } } } } } companion object { - val DEMOTING_ANNOTATION = - listOf("androidx.test.filters.FlakyTest", "android.platform.test.annotations.FlakyTest") + val DEMOTING_ANNOTATION_BUG_ID = + listOf( + "androidx.test.filters.FlakyTest", + "android.platform.test.annotations.FlakyTest", + "android.platform.test.rule.PlatinumRule.Platinum", + ) + + const val DEMOTING_ANNOTATION_IGNORE = "org.junit.Ignore" @JvmField val ISSUE: Issue = diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt index 557c300635eb..63eb2632979c 100644 --- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt +++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/DemotingTestWithoutBugDetectorTest.kt @@ -127,6 +127,139 @@ class DemotingTestWithoutBugDetectorTest : SystemUILintDetectorTest() { ) } + @Test + fun testExcludeDevices_withBugId() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.platform.test.rule.PlatinumRule.Platinum; + + @Platinum(devices = "foo,bar", bugId = 123) + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testExcludeDevices_withoutBugId() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import android.platform.test.rule.PlatinumRule.Platinum; + + @Platinum(devices = "foo,bar") + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:4: Warning: Please attach a bug id to track demoted test [DemotingTestWithoutBug] + @Platinum(devices = "foo,bar") + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + + @Test + fun testIgnore_withBug() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import org.junit.Ignore; + + @Ignore("Blocked by b/123.") + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expectClean() + } + + @Test + fun testIgnore_withoutBug() { + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import org.junit.Ignore; + + @Ignore + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:4: Warning: Please attach a bug (e.g. b/123) to track demoted test [DemotingTestWithoutBug] + @Ignore + ~~~~~~~ + 0 errors, 1 warnings + """ + ) + + lint() + .files( + TestFiles.java( + """ + package test.pkg; + import org.junit.Ignore; + + @Ignore("Not ready") + public class TestClass { + public void testCase() {} + } + """ + ) + .indented(), + *stubs + ) + .issues(DemotingTestWithoutBugDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass.java:4: Warning: Please attach a bug (e.g. b/123) to track demoted test [DemotingTestWithoutBug] + @Ignore("Not ready") + ~~~~~~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + } + private val filtersFlakyTestStub: TestFile = java( """ @@ -147,5 +280,34 @@ class DemotingTestWithoutBugDetectorTest : SystemUILintDetectorTest() { } """ ) - private val stubs = arrayOf(filtersFlakyTestStub, annotationsFlakyTestStub) + private val annotationsPlatinumStub: TestFile = + java( + """ + package android.platform.test.rule; + + public class PlatinumRule { + public @interface Platinum { + String devices(); + int bugId() default -1; + } + } + """ + ) + private val annotationsIgnoreStub: TestFile = + java( + """ + package org.junit; + + public @interface Ignore { + String value() default ""; + } + """ + ) + private val stubs = + arrayOf( + filtersFlakyTestStub, + annotationsFlakyTestStub, + annotationsPlatinumStub, + annotationsIgnoreStub + ) } diff --git a/packages/SystemUI/res-keyguard/values-land/dimens.xml b/packages/SystemUI/res-keyguard/values-land/dimens.xml index f1aa54412b3b..a4e7a5f12db4 100644 --- a/packages/SystemUI/res-keyguard/values-land/dimens.xml +++ b/packages/SystemUI/res-keyguard/values-land/dimens.xml @@ -27,6 +27,4 @@ <integer name="scaled_password_text_size">26</integer> <dimen name="bouncer_user_switcher_y_trans">@dimen/status_bar_height</dimen> - <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> </resources> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 992d143ff66d..edd6eff92c1c 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -123,9 +123,7 @@ <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen> <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen> <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen> - <dimen name="bouncer_user_switcher_y_trans">0dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> + <dimen name="bouncer_user_switcher_y_trans">80dp</dimen> <!-- 2 * the margin + size should equal the plus_margin --> <dimen name="user_switcher_icon_large_margin">16dp</dimen> diff --git a/packages/SystemUI/res/drawable/ic_keyboard_backlight.xml b/packages/SystemUI/res/drawable/ic_keyboard_backlight.xml new file mode 100644 index 000000000000..d123caf82ed5 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_keyboard_backlight.xml @@ -0,0 +1,12 @@ +<vector android:height="11dp" android:viewportHeight="12" + android:viewportWidth="22" android:width="20.166666dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <group> + <clip-path android:pathData="M0,0.5h22v11h-22z"/> + <path android:fillColor="#231F20" android:pathData="M6.397,9.908H0V11.5H6.397V9.908Z"/> + <path android:fillColor="#231F20" android:pathData="M14.199,9.908H7.801V11.5H14.199V9.908Z"/> + <path android:fillColor="#231F20" android:pathData="M11.858,0.5H10.142V6.434H11.858V0.5Z"/> + <path android:fillColor="#231F20" android:pathData="M8.348,7.129L3.885,2.975L3.823,2.932L2.668,4.003L2.621,4.046L7.084,8.2L7.146,8.243L8.301,7.172L8.348,7.129Z"/> + <path android:fillColor="#231F20" android:pathData="M18.224,2.975L18.177,2.932L13.653,7.129L14.807,8.2L14.854,8.243L19.379,4.046L18.224,2.975Z"/> + <path android:fillColor="#231F20" android:pathData="M22,9.908H15.603V11.5H22V9.908Z"/> + </group> +</vector> diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 4f38e6058723..908aac4a7b7f 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -66,4 +66,8 @@ <dimen name="controls_header_horizontal_padding">12dp</dimen> <dimen name="controls_content_margin_horizontal">16dp</dimen> + + <!-- Bouncer user switcher margins --> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml index b98165fb08f0..ca62d286f4ee 100644 --- a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml @@ -21,6 +21,6 @@ <!-- Space between status view and notification shelf --> <dimen name="keyguard_status_view_bottom_margin">70dp</dimen> <dimen name="keyguard_clock_top_margin">80dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">186dp</dimen> - <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">110dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">155dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">85dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index ca4217f64b60..ac4c4929bab2 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -207,6 +207,11 @@ <color name="control_thumbnail_shadow_color">@*android:color/black</color> <color name="controls_task_view_bg">#CC191C1D</color> + <!-- Keyboard backlight indicator--> + <color name="backlight_indicator_step_filled">#F6E388</color> + <color name="backlight_indicator_step_empty">#494740</color> + <color name="backlight_indicator_background">#32302A</color> + <!-- Docked misalignment message --> <color name="misalignment_text_color">#F28B82</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 3b8f1a7d1e4d..35fc9b69e1f4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1653,6 +1653,19 @@ <dimen name="media_output_broadcast_info_summary_height">20dp</dimen> <dimen name="media_output_broadcast_info_edit">18dp</dimen> + <!-- Keyboard backlight indicator--> + <dimen name="backlight_indicator_root_corner_radius">48dp</dimen> + <dimen name="backlight_indicator_root_vertical_padding">8dp</dimen> + <dimen name="backlight_indicator_root_horizontal_padding">4dp</dimen> + <dimen name="backlight_indicator_icon_width">22dp</dimen> + <dimen name="backlight_indicator_icon_height">11dp</dimen> + <dimen name="backlight_indicator_icon_left_margin">2dp</dimen> + <dimen name="backlight_indicator_step_width">52dp</dimen> + <dimen name="backlight_indicator_step_height">40dp</dimen> + <dimen name="backlight_indicator_step_horizontal_margin">4dp</dimen> + <dimen name="backlight_indicator_step_small_radius">4dp</dimen> + <dimen name="backlight_indicator_step_large_radius">48dp</dimen> + <!-- Broadcast dialog --> <dimen name="broadcast_dialog_title_img_margin_top">18dp</dimen> <dimen name="broadcast_dialog_title_text_size">24sp</dimen> @@ -1701,4 +1714,9 @@ it is long-pressed. --> <dimen name="keyguard_long_press_settings_popup_vertical_offset">96dp</dimen> + + + <!-- Bouncer user switcher margins --> + <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">0dp</dimen> + <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 8be599867eb4..9a9f5106b7d8 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2258,7 +2258,7 @@ <!-- Shows in a dialog presented to the user to authorize this app to display a Device controls panel (embedded activity) instead of controls rendered by SystemUI [CHAR LIMIT=NONE] --> - <string name="controls_panel_authorization">When you add <xliff:g id="appName" example="My app">%s</xliff:g>, it can add controls and content to this panel. In some apps, you can choose which controls show up here.</string> + <string name="controls_panel_authorization"><xliff:g id="appName" example="My app">%s</xliff:g>can choose which controls and content show here.</string> <!-- Shows in a dialog presented to the user to authorize this app removal from a Device controls panel [CHAR LIMIT=NONE] --> @@ -2318,7 +2318,7 @@ <!-- Title of the dialog to control certain devices from lock screen without auth [CHAR LIMIT=NONE] --> <string name="controls_settings_trivial_controls_dialog_title">Control devices from lock screen?</string> <!-- Message of the dialog to control certain devices from lock screen without auth [CHAR LIMIT=NONE] --> - <string name="controls_settings_trivial_controls_dialog_message">You can control some devices without unlocking your phone or tablet.\n\nYour device app determines which devices can be controlled in this way.</string> + <string name="controls_settings_trivial_controls_dialog_message">You can control some devices without unlocking your phone or tablet. Your device app determines which devices can be controlled in this way.</string> <!-- Neutral button title of the controls dialog [CHAR LIMIT=NONE] --> <string name="controls_settings_dialog_neutral_button">No thanks</string> <!-- Positive button title of the controls dialog [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt index 9a581aaa9b2c..482158e80d0f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt @@ -91,6 +91,22 @@ constructor( val sampledRegion = calculateSampledRegion(sampledView) val regions = ArrayList<RectF>() val sampledRegionWithOffset = convertBounds(sampledRegion) + + if ( + sampledRegionWithOffset.left < 0.0 || + sampledRegionWithOffset.right > 1.0 || + sampledRegionWithOffset.top < 0.0 || + sampledRegionWithOffset.bottom > 1.0 + ) { + android.util.Log.e( + "RegionSampler", + "view out of bounds: $sampledRegion | " + + "screen width: ${displaySize.x}, screen height: ${displaySize.y}", + Exception() + ) + return + } + regions.add(sampledRegionWithOffset) wallpaperManager?.removeOnColorsChangedListener(this) diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java index 359da13a9799..5b27c40740da 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java @@ -29,8 +29,11 @@ import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.SuppressLint; import android.app.StatusBarManager; +import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; @@ -86,6 +89,7 @@ public class RotationButtonController { private RotationButton mRotationButton; private boolean mIsRecentsAnimationRunning; + private boolean mDocked; private boolean mHomeRotationEnabled; private int mLastRotationSuggestion; private boolean mPendingRotationSuggestion; @@ -123,6 +127,12 @@ public class RotationButtonController { () -> mPendingRotationSuggestion = false; private Animator mRotateHideAnimator; + private final BroadcastReceiver mDockedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateDockedState(intent); + } + }; private final IRotationWatcher.Stub mRotationWatcher = new IRotationWatcher.Stub() { @Override @@ -136,7 +146,8 @@ public class RotationButtonController { // The isVisible check makes the rotation button disappear when we are not locked // (e.g. for tabletop auto-rotate). if (rotationLocked || mRotationButton.isVisible()) { - if (shouldOverrideUserLockPrefs(rotation) && rotationLocked) { + // Do not allow a change in rotation to set user rotation when docked. + if (shouldOverrideUserLockPrefs(rotation) && rotationLocked && !mDocked) { setRotationLockedAtAngle(rotation); } setRotateSuggestionButtonState(false /* visible */, true /* forced */); @@ -214,6 +225,10 @@ public class RotationButtonController { } mListenersRegistered = true; + + updateDockedState(mContext.registerReceiver(mDockedReceiver, + new IntentFilter(Intent.ACTION_DOCK_EVENT))); + try { WindowManagerGlobal.getWindowManagerService() .watchRotation(mRotationWatcher, DEFAULT_DISPLAY); @@ -234,6 +249,8 @@ public class RotationButtonController { } mListenersRegistered = false; + + mContext.unregisterReceiver(mDockedReceiver); try { WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher); } catch (RemoteException e) { @@ -345,6 +362,15 @@ public class RotationButtonController { updateRotationButtonStateInOverview(); } + private void updateDockedState(Intent intent) { + if (intent == null) { + return; + } + + mDocked = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED) + != Intent.EXTRA_DOCK_STATE_UNDOCKED; + } + private void updateRotationButtonStateInOverview() { if (mIsRecentsAnimationRunning && !mHomeRotationEnabled) { setRotateSuggestionButtonState(false, true /* hideImmediately */); diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt index 8323d0971ad7..f005bab55de6 100644 --- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt @@ -23,6 +23,8 @@ import com.android.systemui.util.settings.SettingsUtilModule import dagger.Binds import dagger.Module import dagger.Provides +import dagger.multibindings.IntoSet +import javax.inject.Named @Module(includes = [ FeatureFlagsDebugStartableModule::class, @@ -35,7 +37,8 @@ abstract class FlagsModule { abstract fun bindsFeatureFlagDebug(impl: FeatureFlagsDebug): FeatureFlags @Binds - abstract fun bindsRestarter(debugRestarter: FeatureFlagsDebugRestarter): Restarter + @IntoSet + abstract fun bindsScreenIdleCondition(impl: ScreenIdleCondition): ConditionalRestarter.Condition @Module companion object { @@ -44,5 +47,10 @@ abstract class FlagsModule { fun provideFlagManager(context: Context, @Main handler: Handler): FlagManager { return FlagManager(context, handler) } + + @JvmStatic + @Provides + @Named(ConditionalRestarter.RESTART_DELAY) + fun provideRestartDelaySec(): Long = 1 } } diff --git a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt index 87beff76290d..927d4604b823 100644 --- a/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt +++ b/packages/SystemUI/src-release/com/android/systemui/flags/FlagsModule.kt @@ -18,6 +18,9 @@ package com.android.systemui.flags import dagger.Binds import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoSet +import javax.inject.Named @Module(includes = [ FeatureFlagsReleaseStartableModule::class, @@ -29,5 +32,18 @@ abstract class FlagsModule { abstract fun bindsFeatureFlagRelease(impl: FeatureFlagsRelease): FeatureFlags @Binds - abstract fun bindsRestarter(debugRestarter: FeatureFlagsReleaseRestarter): Restarter + @IntoSet + abstract fun bindsScreenIdleCondition(impl: ScreenIdleCondition): ConditionalRestarter.Condition + + @Binds + @IntoSet + abstract fun bindsPluggedInCondition(impl: PluggedInCondition): ConditionalRestarter.Condition + + @Module + companion object { + @JvmStatic + @Provides + @Named(ConditionalRestarter.RESTART_DELAY) + fun provideRestartDelaySec(): Long = 30 + } } diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 92ee37310130..4aaa566eb852 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -21,6 +21,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.res.Resources +import android.graphics.Rect import android.text.format.DateFormat import android.util.TypedValue import android.view.View @@ -119,10 +120,6 @@ constructor( private val mLayoutChangedListener = object : View.OnLayoutChangeListener { - private var currentSmallClockView: View? = null - private var currentLargeClockView: View? = null - private var currentSmallClockLocation = IntArray(2) - private var currentLargeClockLocation = IntArray(2) override fun onLayoutChange( view: View?, @@ -135,6 +132,8 @@ constructor( oldRight: Int, oldBottom: Int ) { + view?.removeOnLayoutChangeListener(this) + val parent = (view?.parent) as FrameLayout // don't pass in negative bounds when clocks are in transition state @@ -142,31 +141,12 @@ constructor( return } - // SMALL CLOCK - if (parent.id == R.id.lockscreen_clock_view) { - // view bounds have changed due to clock size changing (i.e. different character - // widths) - // AND/OR the view has been translated when transitioning between small and - // large clock - if ( - view != currentSmallClockView || - !view.locationOnScreen.contentEquals(currentSmallClockLocation) - ) { - currentSmallClockView = view - currentSmallClockLocation = view.locationOnScreen - updateRegionSampler(view) - } - } - // LARGE CLOCK - else if (parent.id == R.id.lockscreen_clock_view_large) { - if ( - view != currentLargeClockView || - !view.locationOnScreen.contentEquals(currentLargeClockLocation) - ) { - currentLargeClockView = view - currentLargeClockLocation = view.locationOnScreen - updateRegionSampler(view) - } + val currentViewRect = Rect(left, top, right, bottom) + val oldViewRect = Rect(oldLeft, oldTop, oldRight, oldBottom) + + if (currentViewRect.width() != oldViewRect.width() || + currentViewRect.height() != oldViewRect.height()) { + updateRegionSampler(view) } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 67e3400670ba..03947542d21e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -217,9 +217,11 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { private void animate(float progress) { Interpolator standardDecelerate = Interpolators.STANDARD_DECELERATE; Interpolator legacyDecelerate = Interpolators.LEGACY_DECELERATE; + float standardProgress = standardDecelerate.getInterpolation(progress); mBouncerMessageView.setTranslationY( - mYTrans - mYTrans * standardDecelerate.getInterpolation(progress)); + mYTrans - mYTrans * standardProgress); + mBouncerMessageView.setAlpha(standardProgress); for (int i = 0; i < mViews.length; i++) { View[] row = mViews[i]; @@ -236,7 +238,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { view.setAlpha(scaledProgress); int yDistance = mYTrans + mYTransOffset * i; view.setTranslationY( - yDistance - (yDistance * standardDecelerate.getInterpolation(progress))); + yDistance - (yDistance * standardProgress)); if (view instanceof NumPadAnimationListener) { ((NumPadAnimationListener) view).setProgress(scaledProgress); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 66d5d097ab04..ba5a8c94dc23 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -39,6 +39,7 @@ import static java.lang.Integer.max; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; @@ -1067,10 +1068,14 @@ public class KeyguardSecurityContainer extends ConstraintLayout { int yTranslation = mResources.getDimensionPixelSize(R.dimen.disappear_y_translation); + AnimatorSet anims = new AnimatorSet(); ObjectAnimator yAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, yTranslation); - yAnim.setInterpolator(Interpolators.STANDARD_ACCELERATE); - yAnim.setDuration(500); - yAnim.start(); + ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA, + 0f); + + anims.setInterpolator(Interpolators.STANDARD_ACCELERATE); + anims.playTogether(alphaAnim, yAnim); + anims.start(); } private void setupUserSwitcher() { @@ -1220,8 +1225,7 @@ public class KeyguardSecurityContainer extends ConstraintLayout { constraintSet.connect(rightElement, LEFT, leftElement, RIGHT); constraintSet.connect(rightElement, RIGHT, PARENT_ID, RIGHT); constraintSet.connect(mUserSwitcherViewGroup.getId(), TOP, PARENT_ID, TOP); - constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, PARENT_ID, BOTTOM, - yTrans); + constraintSet.connect(mUserSwitcherViewGroup.getId(), BOTTOM, PARENT_ID, BOTTOM); constraintSet.connect(mViewFlipper.getId(), TOP, PARENT_ID, TOP); constraintSet.connect(mViewFlipper.getId(), BOTTOM, PARENT_ID, BOTTOM); constraintSet.setHorizontalChainStyle(mUserSwitcherViewGroup.getId(), CHAIN_SPREAD); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index c32b8530d589..2c2caea60f5a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -127,6 +127,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private View.OnKeyListener mOnKeyListener = (v, keyCode, event) -> interceptMediaKey(event); private ActivityStarter.OnDismissAction mDismissAction; private Runnable mCancelAction; + private boolean mWillRunDismissFromKeyguard; private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED; @@ -262,8 +263,10 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard // If there's a pending runnable because the user interacted with a widget // and we're leaving keyguard, then run it. boolean deferKeyguardDone = false; + mWillRunDismissFromKeyguard = false; if (mDismissAction != null) { deferKeyguardDone = mDismissAction.onDismiss(); + mWillRunDismissFromKeyguard = mDismissAction.willRunAnimationOnKeyguard(); mDismissAction = null; mCancelAction = null; } @@ -526,6 +529,13 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } /** + * @return will the dismissal run from the keyguard layout (instead of from bouncer) + */ + public boolean willRunDismissFromKeyguard() { + return mWillRunDismissFromKeyguard; + } + + /** * Remove any dismiss action or cancel action that was set. */ public void cancelDismissAction() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 2370c8a27da7..d83af60b1373 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -708,6 +708,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab if (mKeyguardGoingAway) { updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onKeyguardGoingAway(); + } + } } updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } @@ -2433,8 +2439,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab () -> mFaceManager != null && mFaceManager.isHardwareDetected() && mFaceManager.hasEnrolledTemplates(userId) && mBiometricEnabledForUser.get(userId)); + if (mIsFaceEnrolled != isFaceEnrolled) { + mLogger.logFaceEnrolledUpdated(mIsFaceEnrolled, isFaceEnrolled); + } mIsFaceEnrolled = isFaceEnrolled; - mLogger.logFaceEnrolledUpdated(mIsFaceEnrolled, isFaceEnrolled); } public boolean isFaceSupported() { @@ -3094,12 +3102,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @VisibleForTesting boolean isUnlockWithFingerprintPossible(int userId) { // TODO (b/242022358), make this rely on onEnrollmentChanged event and update it only once. - boolean fpEnrolled = mFpm != null && mFpm.isHardwareDetected() + boolean newFpEnrolled = mFpm != null && mFpm.isHardwareDetected() && !isFingerprintDisabled(userId) && mFpm.hasEnrolledTemplates(userId); - mLogger.logFpEnrolledUpdated(userId, - mIsUnlockWithFingerprintPossible.getOrDefault(userId, false), - fpEnrolled); - mIsUnlockWithFingerprintPossible.put(userId, fpEnrolled); + Boolean oldFpEnrolled = mIsUnlockWithFingerprintPossible.getOrDefault(userId, false); + if (oldFpEnrolled != newFpEnrolled) { + mLogger.logFpEnrolledUpdated(userId, oldFpEnrolled, newFpEnrolled); + } + mIsUnlockWithFingerprintPossible.put(userId, newFpEnrolled); return mIsUnlockWithFingerprintPossible.get(userId); } @@ -3644,7 +3653,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * Register to receive notifications about general keyguard information * (see {@link KeyguardUpdateMonitorCallback}. * - * @param callback The callback to register + * @param callback The callback to register. Stay away from passing anonymous instances + * as they will likely be dereferenced. Ensure that the callback is a class + * field to persist it. */ public void registerCallback(KeyguardUpdateMonitorCallback callback) { Assert.isMainThread(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 0d4889a4c39f..feff216310df 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -317,4 +317,9 @@ public class KeyguardUpdateMonitorCallback { * Called when the non-strong biometric state changed. */ public void onNonStrongBiometricAllowedChanged(int userId) { } + + /** + * Called when keyguard is going away or not going away. + */ + public void onKeyguardGoingAway() { } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt index b9f16665944f..cf5ccc5bbf6c 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingController.kt @@ -16,6 +16,7 @@ package com.android.systemui.controls.management +import android.annotation.WorkerThread import android.content.ComponentName import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.util.UserAwareController @@ -33,6 +34,9 @@ interface ControlsListingController : */ fun getCurrentServices(): List<ControlsServiceInfo> + @WorkerThread + fun forceReload() + /** * Get the app label for a given component. * diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt index c81a2c7e2ac6..8ba060e02c3d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt @@ -16,8 +16,11 @@ package com.android.systemui.controls.management +import android.annotation.WorkerThread import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager import android.os.UserHandle import android.service.controls.ControlsProviderService import android.util.Log @@ -65,7 +68,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( private val serviceListingBuilder: (Context) -> ServiceListing, private val userTracker: UserTracker, dumpManager: DumpManager, - featureFlags: FeatureFlags + private val featureFlags: FeatureFlags ) : ControlsListingController, Dumpable { @Inject @@ -97,18 +100,7 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( // After here, `list` is not captured, so we don't risk modifying it outside of the callback backgroundExecutor.execute { if (userChangeInProgress.get() > 0) return@execute - if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) { - val allowAllApps = featureFlags.isEnabled(Flags.APP_PANELS_ALL_APPS_ALLOWED) - newServices.forEach { - it.resolvePanelActivity(allowAllApps) } - } - - if (newServices != availableServices) { - availableServices = newServices - callbacks.forEach { - it.onServicesUpdated(getCurrentServices()) - } - } + updateServices(newServices) } } @@ -120,6 +112,21 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( serviceListing.reload() } + private fun updateServices(newServices: List<ControlsServiceInfo>) { + if (featureFlags.isEnabled(Flags.USE_APP_PANELS)) { + val allowAllApps = featureFlags.isEnabled(Flags.APP_PANELS_ALL_APPS_ALLOWED) + newServices.forEach { + it.resolvePanelActivity(allowAllApps) } + } + + if (newServices != availableServices) { + availableServices = newServices + callbacks.forEach { + it.onServicesUpdated(getCurrentServices()) + } + } + } + override fun changeUser(newUser: UserHandle) { userChangeInProgress.incrementAndGet() serviceListing.setListening(false) @@ -178,6 +185,23 @@ class ControlsListingControllerImpl @VisibleForTesting constructor( override fun getCurrentServices(): List<ControlsServiceInfo> = availableServices.map(ControlsServiceInfo::copy) + @WorkerThread + override fun forceReload() { + val packageManager = context.packageManager + val intent = Intent(ControlsProviderService.SERVICE_CONTROLS) + val user = userTracker.userHandle + val flags = PackageManager.GET_SERVICES or + PackageManager.GET_META_DATA or + PackageManager.MATCH_DIRECT_BOOT_UNAWARE or + PackageManager.MATCH_DIRECT_BOOT_AWARE + val services = packageManager.queryIntentServicesAsUser( + intent, + PackageManager.ResolveInfoFlags.of(flags.toLong()), + user + ).map { ControlsServiceInfo(userTracker.userContext, it.serviceInfo) } + updateServices(services) + } + /** * Get the localized label for the component. * diff --git a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt index 3a4a00c0ccd3..461caccc86d3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt @@ -19,6 +19,7 @@ package com.android.systemui.controls.start import android.content.Context import android.os.UserHandle +import androidx.annotation.WorkerThread import com.android.systemui.CoreStartable import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent @@ -75,11 +76,13 @@ constructor( // Controls is disabled, we don't need this anymore return } - startForUser() + executor.execute(this::startForUser) userTracker.addCallback(userTrackerCallback, executor) } + @WorkerThread private fun startForUser() { + controlsListingController.forceReload() selectDefaultPanelIfNecessary() bindToPanel() } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index cedc226ae0a9..05527bd2c843 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -70,6 +70,8 @@ import com.android.systemui.security.data.repository.SecurityRepositoryModule; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.dagger.MultiUserUtilsModule; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolatorImpl; import com.android.systemui.smartspace.dagger.SmartspaceModule; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; @@ -306,4 +308,8 @@ public abstract class SystemUIModule { @Binds abstract FgsManagerController bindFgsManagerController(FgsManagerControllerImpl impl); + + @Binds + abstract LargeScreenShadeInterpolator largeScreensShadeInterpolator( + LargeScreenShadeInterpolatorImpl impl); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java index 24e90f066622..aad209090a21 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java @@ -61,9 +61,6 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay @VisibleForTesting boolean mIsAnimationEnabled; - // Whether dream entry animations are finished. - private boolean mEntryAnimationsFinished = false; - @Inject protected ComplicationHostViewController( @Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout view, @@ -78,14 +75,6 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay mComplicationCollectionViewModel = viewModel; mDreamOverlayStateController = dreamOverlayStateController; - mDreamOverlayStateController.addCallback(new DreamOverlayStateController.Callback() { - @Override - public void onStateChanged() { - mEntryAnimationsFinished = - mDreamOverlayStateController.areEntryAnimationsFinished(); - } - }); - // Whether animations are enabled. mIsAnimationEnabled = secureSettings.getFloatForUser( Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f, UserHandle.USER_CURRENT) != 0.0f; @@ -159,7 +148,8 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay // Complications to be added before dream entry animations are finished are set // to invisible and are animated in. - if (!mEntryAnimationsFinished && mIsAnimationEnabled) { + if (!mDreamOverlayStateController.areEntryAnimationsFinished() + && mIsAnimationEnabled) { view.setVisibility(View.INVISIBLE); } mComplications.put(id, viewHolder); diff --git a/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt new file mode 100644 index 000000000000..b20e33a63776 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/ConditionalRestarter.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import android.util.Log +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** Restarts the process after all passed in [Condition]s are true. */ +class ConditionalRestarter +@Inject +constructor( + private val systemExitRestarter: SystemExitRestarter, + private val conditions: Set<@JvmSuppressWildcards Condition>, + @Named(RESTART_DELAY) private val restartDelaySec: Long, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : Restarter { + + private var restartJob: Job? = null + private var pendingReason = "" + private var androidRestartRequested = false + + override fun restartSystemUI(reason: String) { + Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting when idle.") + scheduleRestart(reason) + } + + override fun restartAndroid(reason: String) { + Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting when idle.") + androidRestartRequested = true + scheduleRestart(reason) + } + + private fun scheduleRestart(reason: String = "") { + pendingReason = if (reason.isEmpty()) pendingReason else reason + + if (conditions.all { c -> c.canRestartNow(this::scheduleRestart) }) { + if (restartJob == null) { + restartJob = + applicationScope.launch(backgroundDispatcher) { + delay(TimeUnit.SECONDS.toMillis(restartDelaySec)) + restartNow() + } + } + } else { + restartJob?.cancel() + restartJob = null + } + } + + private fun restartNow() { + if (androidRestartRequested) { + systemExitRestarter.restartAndroid(pendingReason) + } else { + systemExitRestarter.restartSystemUI(pendingReason) + } + } + + interface Condition { + /** + * Should return true if the system is ready to restart. + * + * A call to this function means that we want to restart and are waiting for this condition + * to return true. + * + * retryFn should be cached if it is _not_ ready to restart, and later called when it _is_ + * ready to restart. At that point, this method will be called again to verify that the + * system is ready. + * + * Multiple calls to an instance of this method may happen for a single restart attempt if + * multiple [Condition]s are being checked. If any one [Condition] returns false, all the + * [Condition]s will need to be rechecked on the next restart attempt. + */ + fun canRestartNow(retryFn: () -> Unit): Boolean + } + + companion object { + const val RESTART_DELAY = "restarter_restart_delay" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt deleted file mode 100644 index a6956a443e46..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebugRestarter.kt +++ /dev/null @@ -1,70 +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.flags - -import android.util.Log -import com.android.systemui.keyguard.WakefulnessLifecycle -import javax.inject.Inject - -/** Restarts SystemUI when the screen is locked. */ -class FeatureFlagsDebugRestarter -@Inject -constructor( - private val wakefulnessLifecycle: WakefulnessLifecycle, - private val systemExitRestarter: SystemExitRestarter, -) : Restarter { - - private var androidRestartRequested = false - private var pendingReason = "" - - val observer = - object : WakefulnessLifecycle.Observer { - override fun onFinishedGoingToSleep() { - Log.d(FeatureFlagsDebug.TAG, "Restarting due to systemui flag change") - restartNow() - } - } - - override fun restartSystemUI(reason: String) { - Log.d(FeatureFlagsDebug.TAG, "SystemUI Restart requested. Restarting on next screen off.") - Log.i(FeatureFlagsDebug.TAG, reason) - scheduleRestart(reason) - } - - override fun restartAndroid(reason: String) { - Log.d(FeatureFlagsDebug.TAG, "Android Restart requested. Restarting on next screen off.") - androidRestartRequested = true - scheduleRestart(reason) - } - - fun scheduleRestart(reason: String) { - pendingReason = reason - if (wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP) { - restartNow() - } else { - wakefulnessLifecycle.addObserver(observer) - } - } - - private fun restartNow() { - if (androidRestartRequested) { - systemExitRestarter.restartAndroid(pendingReason) - } else { - systemExitRestarter.restartSystemUI(pendingReason) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt deleted file mode 100644 index c08266caf147..000000000000 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsReleaseRestarter.kt +++ /dev/null @@ -1,101 +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.flags - -import android.util.Log -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP -import com.android.systemui.statusbar.policy.BatteryController -import com.android.systemui.util.concurrency.DelayableExecutor -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -/** Restarts SystemUI when the device appears idle. */ -class FeatureFlagsReleaseRestarter -@Inject -constructor( - private val wakefulnessLifecycle: WakefulnessLifecycle, - private val batteryController: BatteryController, - @Background private val bgExecutor: DelayableExecutor, - private val systemExitRestarter: SystemExitRestarter -) : Restarter { - var listenersAdded = false - var pendingRestart: Runnable? = null - private var pendingReason = "" - var androidRestartRequested = false - - val observer = - object : WakefulnessLifecycle.Observer { - override fun onFinishedGoingToSleep() { - scheduleRestart(pendingReason) - } - } - - val batteryCallback = - object : BatteryController.BatteryStateChangeCallback { - override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { - scheduleRestart(pendingReason) - } - } - - override fun restartSystemUI(reason: String) { - Log.d( - FeatureFlagsDebug.TAG, - "SystemUI Restart requested. Restarting when plugged in and idle." - ) - scheduleRestart(reason) - } - - override fun restartAndroid(reason: String) { - Log.d( - FeatureFlagsDebug.TAG, - "Android Restart requested. Restarting when plugged in and idle." - ) - androidRestartRequested = true - scheduleRestart(reason) - } - - private fun scheduleRestart(reason: String) { - // Don't bother adding listeners twice. - pendingReason = reason - if (!listenersAdded) { - listenersAdded = true - wakefulnessLifecycle.addObserver(observer) - batteryController.addCallback(batteryCallback) - } - if ( - wakefulnessLifecycle.wakefulness == WAKEFULNESS_ASLEEP && batteryController.isPluggedIn - ) { - if (pendingRestart == null) { - pendingRestart = bgExecutor.executeDelayed(this::restartNow, 30L, TimeUnit.SECONDS) - } - } else if (pendingRestart != null) { - pendingRestart?.run() - pendingRestart = null - } - } - - private fun restartNow() { - Log.d(FeatureFlagsRelease.TAG, "Restarting due to systemui flag change") - if (androidRestartRequested) { - systemExitRestarter.restartAndroid(pendingReason) - } else { - systemExitRestarter.restartSystemUI(pendingReason) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 6d0d893a60f2..8b66b00e8670 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -117,6 +117,9 @@ object Flags { val ANIMATED_NOTIFICATION_SHADE_INSETS = unreleasedFlag(270682168, "animated_notification_shade_insets", teamfood = true) + // TODO(b/268005230): Tracking Bug + @JvmField val SENSITIVE_REVEAL_ANIM = unreleasedFlag(268005230, "sensitive_reveal_anim") + // 200 - keyguard/lockscreen // ** Flag retired ** // public static final BooleanFlag KEYGUARD_LAYOUT = @@ -140,7 +143,7 @@ object Flags { * the digits when the clock moves. */ @JvmField - val STEP_CLOCK_ANIMATION = unreleasedFlag(212, "step_clock_animation", teamfood = true) + val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation") /** * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository @@ -218,6 +221,11 @@ object Flags { teamfood = true, ) + /** Whether to inflate the bouncer view on a background thread. */ + // TODO(b/272091103): Tracking Bug + @JvmField + val ASYNC_INFLATE_BOUNCER = unreleasedFlag(229, "async_inflate_bouncer", teamfood = true) + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite") @@ -388,13 +396,16 @@ object Flags { @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple") // TODO(b/270882464): Tracking Bug - val ENABLE_DOCK_SETUP_V2 = unreleasedFlag(1005, "enable_dock_setup_v2") + val ENABLE_DOCK_SETUP_V2 = unreleasedFlag(1005, "enable_dock_setup_v2", teamfood = true) // TODO(b/265045965): Tracking Bug val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot") @JvmField - val ENABLE_LOW_LIGHT_CLOCK_UNDOCKED = unreleasedFlag(1004, "enable_low_light_clock_undocked") + // TODO(b/271428141): Tracking Bug + val ENABLE_LOW_LIGHT_CLOCK_UNDOCKED = unreleasedFlag( + 1004, + "enable_low_light_clock_undocked", teamfood = true) // 1100 - windowing @Keep @@ -482,6 +493,13 @@ object Flags { val ENABLE_PIP_APP_ICON_OVERLAY = sysPropBooleanFlag(1115, "persist.wm.debug.enable_pip_app_icon_overlay", default = true) + // TODO(b/272110828): Tracking bug + @Keep + @JvmField + val ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP = + sysPropBooleanFlag( + 1116, "persist.wm.debug.enable_move_floating_window_in_tabletop", default = false) + // 1200 - predictive back @Keep @JvmField @@ -590,7 +608,7 @@ object Flags { @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = releasedFlag(1800, "leave_shade_open_for_bugreport") // TODO(b/265944639): Tracking Bug - @JvmField val DUAL_SHADE = releasedFlag(1801, "dual_shade") + @JvmField val DUAL_SHADE = unreleasedFlag(1801, "dual_shade") // 1900 @JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag") @@ -662,4 +680,9 @@ object Flags { // TODO(b/259428678): Tracking Bug @JvmField val KEYBOARD_BACKLIGHT_INDICATOR = unreleasedFlag(2601, "keyboard_backlight_indicator") + + // TODO(b/272036292): Tracking Bug + @JvmField + val LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION = + unreleasedFlag(2602, "large_shade_granular_alpha_interpolation", teamfood = true) } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt index 0054d266c283..3c5012559a89 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagsCommonModule.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.flags +import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Named @@ -22,6 +23,8 @@ import javax.inject.Named /** Module containing shared code for all FeatureFlag implementations. */ @Module interface FlagsCommonModule { + @Binds fun bindsRestarter(impl: ConditionalRestarter): Restarter + companion object { const val ALL_FLAGS = "all_flags" diff --git a/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt new file mode 100644 index 000000000000..3120638cb17f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.statusbar.policy.BatteryController +import javax.inject.Inject + +/** Returns true when the device is plugged in. */ +class PluggedInCondition +@Inject +constructor( + private val batteryController: BatteryController, +) : ConditionalRestarter.Condition { + + var listenersAdded = false + var retryFn: (() -> Unit)? = null + + val batteryCallback = + object : BatteryController.BatteryStateChangeCallback { + override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) { + retryFn?.invoke() + } + } + + override fun canRestartNow(retryFn: () -> Unit): Boolean { + if (!listenersAdded) { + listenersAdded = true + batteryController.addCallback(batteryCallback) + } + + this.retryFn = retryFn + + return batteryController.isPluggedIn + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt b/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt new file mode 100644 index 000000000000..49e61afbdcd6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/ScreenIdleCondition.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.keyguard.WakefulnessLifecycle +import javax.inject.Inject + +/** Returns true when the device is "asleep" as defined by the [WakefullnessLifecycle]. */ +class ScreenIdleCondition +@Inject +constructor( + private val wakefulnessLifecycle: WakefulnessLifecycle, +) : ConditionalRestarter.Condition { + + var listenersAdded = false + var retryFn: (() -> Unit)? = null + + val observer = + object : WakefulnessLifecycle.Observer { + override fun onFinishedGoingToSleep() { + retryFn?.invoke() + } + } + + override fun canRestartNow(retryFn: () -> Unit): Boolean { + if (!listenersAdded) { + listenersAdded = true + wakefulnessLifecycle.addObserver(observer) + } + + this.retryFn = retryFn + + return wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt index 85d0379a77db..5e806b612805 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinator.kt @@ -46,8 +46,15 @@ constructor( viewModel.dialogContent.collect { dialogViewModel -> if (dialogViewModel != null) { if (dialog == null) { - dialog = KeyboardBacklightDialog(context, dialogViewModel) - // pass viewModel and show + dialog = + KeyboardBacklightDialog( + context, + initialCurrentLevel = dialogViewModel.currentValue, + initialMaxLevel = dialogViewModel.maxValue + ) + dialog?.show() + } else { + dialog?.updateState(dialogViewModel.currentValue, dialogViewModel.maxValue) } } else { dialog?.dismiss() diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt index b68a2a84b5d1..a173f8b914db 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt @@ -17,16 +17,260 @@ package com.android.systemui.keyboard.backlight.ui.view +import android.annotation.ColorInt import android.app.Dialog import android.content.Context +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape import android.os.Bundle -import com.android.systemui.keyboard.backlight.ui.viewmodel.BacklightDialogContentViewModel +import android.view.Gravity +import android.view.Window +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.LinearLayout.LayoutParams +import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT +import com.android.systemui.R +import com.android.systemui.util.children -class KeyboardBacklightDialog(context: Context, val viewModel: BacklightDialogContentViewModel) : - Dialog(context) { +class KeyboardBacklightDialog( + context: Context, + initialCurrentLevel: Int, + initialMaxLevel: Int, +) : Dialog(context) { + + private data class RootProperties( + val cornerRadius: Float, + val verticalPadding: Int, + val horizontalPadding: Int, + ) + + private data class BacklightIconProperties( + val width: Int, + val height: Int, + val leftMargin: Int, + ) + + private data class StepViewProperties( + val width: Int, + val height: Int, + val horizontalMargin: Int, + val smallRadius: Float, + val largeRadius: Float, + ) + + private var currentLevel: Int = 0 + private var maxLevel: Int = 0 + + private lateinit var rootView: LinearLayout + + private var dialogBottomMargin = 208 + private lateinit var rootProperties: RootProperties + private lateinit var iconProperties: BacklightIconProperties + private lateinit var stepProperties: StepViewProperties + @ColorInt var filledRectangleColor: Int = 0 + @ColorInt var emptyRectangleColor: Int = 0 + @ColorInt var backgroundColor: Int = 0 + + init { + currentLevel = initialCurrentLevel + maxLevel = initialMaxLevel + } override fun onCreate(savedInstanceState: Bundle?) { + setUpWindowProperties(this) + setWindowTitle() + updateResources() + rootView = buildRootView() + setContentView(rootView) super.onCreate(savedInstanceState) - // TODO(b/268650355) Implement the dialog + updateState(currentLevel, maxLevel, forceRefresh = true) + } + + private fun updateResources() { + context.resources.apply { + filledRectangleColor = getColor(R.color.backlight_indicator_step_filled) + emptyRectangleColor = getColor(R.color.backlight_indicator_step_empty) + backgroundColor = getColor(R.color.backlight_indicator_background) + rootProperties = + RootProperties( + cornerRadius = + getDimensionPixelSize(R.dimen.backlight_indicator_root_corner_radius) + .toFloat(), + verticalPadding = + getDimensionPixelSize(R.dimen.backlight_indicator_root_vertical_padding), + horizontalPadding = + getDimensionPixelSize(R.dimen.backlight_indicator_root_horizontal_padding) + ) + iconProperties = + BacklightIconProperties( + width = getDimensionPixelSize(R.dimen.backlight_indicator_icon_width), + height = getDimensionPixelSize(R.dimen.backlight_indicator_icon_height), + leftMargin = + getDimensionPixelSize(R.dimen.backlight_indicator_icon_left_margin), + ) + stepProperties = + StepViewProperties( + width = getDimensionPixelSize(R.dimen.backlight_indicator_step_width), + height = getDimensionPixelSize(R.dimen.backlight_indicator_step_height), + horizontalMargin = + getDimensionPixelSize(R.dimen.backlight_indicator_step_horizontal_margin), + smallRadius = + getDimensionPixelSize(R.dimen.backlight_indicator_step_small_radius) + .toFloat(), + largeRadius = + getDimensionPixelSize(R.dimen.backlight_indicator_step_large_radius) + .toFloat(), + ) + } + } + + fun updateState(current: Int, max: Int, forceRefresh: Boolean = false) { + if (maxLevel != max || forceRefresh) { + maxLevel = max + rootView.removeAllViews() + buildStepViews().forEach { rootView.addView(it) } + } + currentLevel = current + updateLevel() + } + + private fun updateLevel() { + rootView.children.forEachIndexed( + action = { index, v -> + val drawable = v.background as ShapeDrawable + if (index <= currentLevel) { + updateColor(drawable, filledRectangleColor) + } else { + updateColor(drawable, emptyRectangleColor) + } + } + ) + } + + private fun updateColor(drawable: ShapeDrawable, @ColorInt color: Int) { + if (drawable.paint.color != color) { + drawable.paint.color = color + drawable.invalidateSelf() + } + } + + private fun buildRootView(): LinearLayout { + val linearLayout = + LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) + setPadding( + /* left= */ rootProperties.horizontalPadding, + /* top= */ rootProperties.verticalPadding, + /* right= */ rootProperties.horizontalPadding, + /* bottom= */ rootProperties.verticalPadding + ) + } + val drawable = + ShapeDrawable( + RoundRectShape( + /* outerRadii= */ FloatArray(8) { rootProperties.cornerRadius }, + /* inset= */ null, + /* innerRadii= */ null + ) + ) + drawable.paint.color = backgroundColor + linearLayout.background = drawable + return linearLayout + } + + private fun buildStepViews(): List<FrameLayout> { + val stepViews = (0..maxLevel).map { i -> createStepViewAt(i) } + stepViews[0].addView(createBacklightIconView()) + return stepViews + } + + private fun createStepViewAt(i: Int): FrameLayout { + return FrameLayout(context).apply { + layoutParams = + FrameLayout.LayoutParams(stepProperties.width, stepProperties.height).apply { + setMargins( + /* left= */ stepProperties.horizontalMargin, + /* top= */ 0, + /* right= */ stepProperties.horizontalMargin, + /* bottom= */ 0 + ) + } + val drawable = + ShapeDrawable( + RoundRectShape( + /* outerRadii= */ radiiForIndex(i, maxLevel), + /* inset= */ null, + /* innerRadii= */ null + ) + ) + drawable.paint.color = emptyRectangleColor + background = drawable + } + } + + private fun createBacklightIconView(): ImageView { + return ImageView(context).apply { + setImageResource(R.drawable.ic_keyboard_backlight) + layoutParams = + FrameLayout.LayoutParams(iconProperties.width, iconProperties.height).apply { + gravity = Gravity.CENTER + leftMargin = iconProperties.leftMargin + } + } + } + + private fun setWindowTitle() { + val attrs = window.attributes + // TODO(b/271796169): check if title needs to be a translatable resource. + attrs.title = "KeyboardBacklightDialog" + attrs?.y = dialogBottomMargin + window.attributes = attrs + } + + private fun setUpWindowProperties(dialog: Dialog) { + val window = dialog.window + window.requestFeature(Window.FEATURE_NO_TITLE) // otherwise fails while creating actionBar + window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + window.addFlags( + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + ) + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + window.setBackgroundDrawableResource(android.R.color.transparent) + window.setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL) + setCanceledOnTouchOutside(true) + } + + private fun radiiForIndex(i: Int, last: Int): FloatArray { + val smallRadius = stepProperties.smallRadius + val largeRadius = stepProperties.largeRadius + return when (i) { + 0 -> // left radii bigger + floatArrayOf( + largeRadius, + largeRadius, + smallRadius, + smallRadius, + smallRadius, + smallRadius, + largeRadius, + largeRadius + ) + last -> // right radii bigger + floatArrayOf( + smallRadius, + smallRadius, + largeRadius, + largeRadius, + largeRadius, + largeRadius, + smallRadius, + smallRadius + ) + else -> FloatArray(8) { smallRadius } // all radii equal + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 8ae171f9264f..90562dc4a243 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -380,10 +380,7 @@ class KeyguardUnlockAnimationController @Inject constructor( // If the launcher is underneath, but we're about to launch an activity, don't do // the animations since they won't be visible. !notificationShadeWindowController.isLaunchingActivity && - launcherUnlockController != null && - // Temporarily disable for foldables since foldable launcher has two first pages, - // which breaks the in-window animation. - !isFoldable(context) + launcherUnlockController != null } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 85554ac9ae44..2815df68ad0a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -964,13 +964,24 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { + if (!handleOnAnimationStart( + transit, apps, wallpapers, nonApps, finishedCallback)) { + // Usually we rely on animation completion to synchronize occluded status, + // but there was no animation to play, so just update it now. + setOccluded(true /* isOccluded */, false /* animate */); + } + } + + private boolean handleOnAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { if (apps == null || apps.length == 0 || apps[0] == null) { if (DEBUG) { Log.d(TAG, "No apps provided to the OccludeByDream runner; " + "skipping occluding animation."); } finishedCallback.onAnimationFinished(); - return; + return false; } final RemoteAnimationTarget primary = apps[0]; @@ -980,7 +991,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.w(TAG, "The occluding app isn't Dream; " + "finishing up. Please check that the config is correct."); finishedCallback.onAnimationFinished(); - return; + return false; } final SyncRtSurfaceTransactionApplier applier = @@ -1029,6 +1040,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mOccludeByDreamAnimator.start(); }); + return true; } }; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt index faeb48526ae4..a2589d3d4116 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt @@ -52,6 +52,7 @@ interface BouncerViewDelegate { cancelAction: Runnable?, ) fun willDismissWithActions(): Boolean + fun willRunDismissFromKeyguard(): Boolean /** @return the {@link OnBackAnimationCallback} to animate Bouncer during a back gesture. */ fun getBackCallback(): OnBackAnimationCallback } 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 index 911861ddde47..28cc69758308 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -64,7 +64,11 @@ constructor( .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) .collect { pair -> val (isAbleToDream, lastStartedTransition) = pair - if (isAbleToDream && lastStartedTransition.to == KeyguardState.LOCKSCREEN) { + if ( + isAbleToDream && + lastStartedTransition.to == KeyguardState.LOCKSCREEN && + lastStartedTransition.from != KeyguardState.AOD + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 2dc8fee25379..1fbfff95ab7e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -47,6 +47,7 @@ constructor( listenForOccludedToLockscreen() listenForOccludedToDreaming() listenForOccludedToAodOrDozing() + listenForOccludedToGone() } private fun listenForOccludedToDreaming() { @@ -72,11 +73,22 @@ constructor( private fun listenForOccludedToLockscreen() { scope.launch { keyguardInteractor.isKeyguardOccluded - .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { (isOccluded, lastStartedKeyguardState) -> + .sample( + combine( + keyguardInteractor.isKeyguardShowing, + keyguardTransitionInteractor.startedKeyguardTransitionStep, + ::Pair + ), + ::toTriple + ) + .collect { (isOccluded, isShowing, lastStartedKeyguardState) -> // Occlusion signals come from the framework, and should interrupt any // existing transition - if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) { + if ( + !isOccluded && + isShowing && + lastStartedKeyguardState.to == KeyguardState.OCCLUDED + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, @@ -90,6 +102,38 @@ constructor( } } + private fun listenForOccludedToGone() { + scope.launch { + keyguardInteractor.isKeyguardOccluded + .sample( + combine( + keyguardInteractor.isKeyguardShowing, + keyguardTransitionInteractor.startedKeyguardTransitionStep, + ::Pair + ), + ::toTriple + ) + .collect { (isOccluded, isShowing, lastStartedKeyguardState) -> + // Occlusion signals come from the framework, and should interrupt any + // existing transition + if ( + !isOccluded && + !isShowing && + lastStartedKeyguardState.to == KeyguardState.OCCLUDED + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.OCCLUDED, + KeyguardState.GONE, + getAnimator(), + ) + ) + } + } + } + } + private fun listenForOccludedToAodOrDozing() { scope.launch { keyguardInteractor.wakefulnessModel 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 ec99049b42e3..c42e5028e18c 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 @@ -142,6 +142,8 @@ constructor( val alternateBouncerShowing: Flow<Boolean> = bouncerRepository.alternateBouncerVisible /** Observable for the [StatusBarState] */ val statusBarState: Flow<StatusBarState> = repository.statusBarState + /** Whether or not quick settings or quick quick settings are showing. */ + val isQuickSettingsVisible: Flow<Boolean> = repository.isQuickSettingsVisible /** * Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear, * side, under display) is used to unlock the device. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 568cc0f42639..66f87bade308 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -93,8 +93,9 @@ constructor( quickAffordanceAlwaysVisible(position), keyguardInteractor.isDozing, keyguardInteractor.isKeyguardShowing, - ) { affordance, isDozing, isKeyguardShowing -> - if (!isDozing && isKeyguardShowing) { + keyguardInteractor.isQuickSettingsVisible + ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible -> + if (!isDozing && isKeyguardShowing && !isQuickSettingsVisible) { affordance } else { KeyguardQuickAffordanceModel.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt index c709fd18298c..a263562b5a7e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt @@ -122,21 +122,24 @@ constructor( val isInteractable: Flow<Boolean> = bouncerExpansion.map { it > 0.9 } val sideFpsShowing: Flow<Boolean> = repository.sideFpsShowing + /** + * This callback needs to be a class field so it does not get garbage collected. + */ + val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { + override fun onBiometricRunningStateChanged( + running: Boolean, + biometricSourceType: BiometricSourceType? + ) { + updateSideFpsVisibility() + } + + override fun onStrongAuthStateChanged(userId: Int) { + updateSideFpsVisibility() + } + } + init { - keyguardUpdateMonitor.registerCallback( - object : KeyguardUpdateMonitorCallback() { - override fun onBiometricRunningStateChanged( - running: Boolean, - biometricSourceType: BiometricSourceType? - ) { - updateSideFpsVisibility() - } - - override fun onStrongAuthStateChanged(userId: Int) { - updateSideFpsVisibility() - } - } - ) + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) } // TODO(b/243685699): Move isScrimmed logic to data layer. @@ -377,6 +380,11 @@ constructor( return primaryBouncerView.delegate?.willDismissWithActions() == true } + /** Will the dismissal run from the keyguard layout (instead of from bouncer) */ + fun willRunDismissFromKeyguard(): Boolean { + return primaryBouncerView.delegate?.willRunDismissFromKeyguard() == true + } + /** Returns whether the bouncer should be full screen. */ private fun needsFullscreenBouncer(): Boolean { val mode: KeyguardSecurityModel.SecurityMode = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScrimAlpha.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScrimAlpha.kt new file mode 100644 index 000000000000..1db77336109e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScrimAlpha.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** Alpha values for scrim updates */ +data class ScrimAlpha( + val frontAlpha: Float = 0f, + val behindAlpha: Float = 0f, + val notificationsAlpha: Float = 0f, +) 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 2337ffc35fa6..bb617bd50c69 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 @@ -97,6 +97,10 @@ object KeyguardBouncerViewBinder { override fun willDismissWithActions(): Boolean { return securityContainerController.hasDismissActions() } + + override fun willRunDismissFromKeyguard(): Boolean { + return securityContainerController.willRunDismissFromKeyguard() + } } view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 92038e24edf3..b23247c30256 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -20,11 +20,14 @@ import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GONE_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.keyguard.shared.model.ScrimAlpha import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.statusbar.SysuiStatusBarStateController import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Breaks down PRIMARY_BOUNCER->GONE transition into discrete steps for corresponding views to @@ -36,6 +39,7 @@ class PrimaryBouncerToGoneTransitionViewModel constructor( private val interactor: KeyguardTransitionInteractor, private val statusBarStateController: SysuiStatusBarStateController, + private val primaryBouncerInteractor: PrimaryBouncerInteractor, ) { private val transitionAnimation = KeyguardTransitionAnimationFlow( @@ -44,26 +48,49 @@ constructor( ) private var leaveShadeOpen: Boolean = false + private var willRunDismissFromKeyguard: Boolean = false /** Bouncer container alpha */ val bouncerAlpha: Flow<Float> = transitionAnimation.createFlow( duration = 200.milliseconds, - onStep = { 1f - it }, - ) - - /** Scrim behind alpha */ - val scrimBehindAlpha: Flow<Float> = - transitionAnimation.createFlow( - duration = TO_GONE_DURATION, - interpolator = EMPHASIZED_ACCELERATE, - onStart = { leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() }, + onStart = { + willRunDismissFromKeyguard = primaryBouncerInteractor.willRunDismissFromKeyguard() + }, onStep = { - if (leaveShadeOpen) { - 1f + if (willRunDismissFromKeyguard) { + 0f } else { 1f - it } }, ) + + /** Scrim alpha values */ + val scrimAlpha: Flow<ScrimAlpha> = + transitionAnimation + .createFlow( + duration = TO_GONE_DURATION, + interpolator = EMPHASIZED_ACCELERATE, + onStart = { + leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() + willRunDismissFromKeyguard = + primaryBouncerInteractor.willRunDismissFromKeyguard() + }, + onStep = { 1f - it }, + ) + .map { + if (willRunDismissFromKeyguard) { + ScrimAlpha( + notificationsAlpha = 1f, + ) + } else if (leaveShadeOpen) { + ScrimAlpha( + behindAlpha = 1f, + notificationsAlpha = 1f, + ) + } else { + ScrimAlpha(behindAlpha = it) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt index 4880f80e7716..b73ddc50f831 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt @@ -59,6 +59,17 @@ data class TableChange( int = value } + /** Updates this to store the same value as [change]. */ + fun updateTo(change: TableChange) { + reset(change.timestamp, change.columnPrefix, change.columnName) + when (change.type) { + DataType.STRING -> set(change.str) + DataType.INT -> set(change.int) + DataType.BOOLEAN -> set(change.bool) + DataType.EMPTY -> {} + } + } + /** Returns true if this object has a change. */ fun hasData(): Boolean { return columnName.isNotBlank() && type != DataType.EMPTY diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt index 29f273a5ed41..a0f1c959aed6 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt @@ -16,13 +16,13 @@ package com.android.systemui.log.table +import android.os.Trace import com.android.systemui.Dumpable import com.android.systemui.plugins.util.RingBuffer import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import java.text.SimpleDateFormat import java.util.Locale -import kotlinx.coroutines.flow.Flow /** * A logger that logs changes in table format. @@ -82,6 +82,19 @@ class TableLogBuffer( private val buffer = RingBuffer(maxSize) { TableChange() } + // Stores the most recently evicted value for each column name (indexed on column name). + // + // Why it's necessary: Because we use a RingBuffer of a fixed size, it's possible that a column + // that's logged infrequently will eventually get pushed out by a different column that's + // logged more frequently. Then, that infrequently-logged column isn't present in the RingBuffer + // at all and we have no logs that the column ever existed. This is a problem because the + // column's information is still relevant, valid, and may be critical to debugging issues. + // + // Fix: When a change is being evicted from the RingBuffer, we store it in this map (based on + // its [TableChange.getName()]. This ensures that we always have at least one value for every + // column ever logged. See b/272016422 for more details. + private val lastEvictedValues = mutableMapOf<String, TableChange>() + // A [TableRowLogger] object, re-used each time [logDiffs] is called. // (Re-used to avoid object allocation.) private val tempRow = TableRowLoggerImpl(0, columnPrefix = "", this) @@ -138,18 +151,24 @@ class TableLogBuffer( // timestamps.) private fun logChange(timestamp: Long, prefix: String, columnName: String, value: String?) { + Trace.beginSection("TableLogBuffer#logChange(string)") val change = obtain(timestamp, prefix, columnName) change.set(value) + Trace.endSection() } private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Boolean) { + Trace.beginSection("TableLogBuffer#logChange(boolean)") val change = obtain(timestamp, prefix, columnName) change.set(value) + Trace.endSection() } private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int?) { + Trace.beginSection("TableLogBuffer#logChange(int)") val change = obtain(timestamp, prefix, columnName) change.set(value) + Trace.endSection() } // TODO(b/259454430): Add additional change types here. @@ -158,6 +177,9 @@ class TableLogBuffer( private fun obtain(timestamp: Long, prefix: String, columnName: String): TableChange { verifyValidName(prefix, columnName) val tableChange = buffer.advance() + if (tableChange.hasData()) { + saveEvictedValue(tableChange) + } tableChange.reset(timestamp, prefix, columnName) return tableChange } @@ -173,10 +195,23 @@ class TableLogBuffer( } } + private fun saveEvictedValue(change: TableChange) { + Trace.beginSection("TableLogBuffer#saveEvictedValue") + val name = change.getName() + val previouslyEvicted = + lastEvictedValues[name] ?: TableChange().also { lastEvictedValues[name] = it } + // For recycling purposes, update the existing object in the map with the new information + // instead of creating a new object. + previouslyEvicted.updateTo(change) + Trace.endSection() + } + @Synchronized override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println(HEADER_PREFIX + name) pw.println("version $VERSION") + + lastEvictedValues.values.sortedBy { it.timestamp }.forEach { it.dump(pw) } for (i in 0 until buffer.size) { buffer[i].dump(pw) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 6023bc250b1b..525b2fcb8dbc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -1343,13 +1343,9 @@ class MediaDataManager( fun onNotificationRemoved(key: String) { Assert.isMainThread() val removed = mediaEntries.remove(key) ?: return - val isEligibleForResume = - removed.isLocalSession() || - (mediaFlags.isRemoteResumeAllowed() && - removed.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } else if (useMediaResumption && removed.resumeAction != null && isEligibleForResume) { + } else if (isAbleToResume(removed)) { convertToResumePlayer(key, removed) } else if (mediaFlags.isRetainingPlayersEnabled()) { handlePossibleRemoval(key, removed, notificationRemoved = true) @@ -1369,6 +1365,14 @@ class MediaDataManager( handlePossibleRemoval(key, updated) } + private fun isAbleToResume(data: MediaData): Boolean { + val isEligibleForResume = + data.isLocalSession() || + (mediaFlags.isRemoteResumeAllowed() && + data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) + return useMediaResumption && data.resumeAction != null && isEligibleForResume + } + /** * Convert to resume state if the player is no longer valid and active, then notify listeners * that the data was updated. Does not convert to resume state if the player is still valid, or @@ -1391,8 +1395,9 @@ class MediaDataManager( if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") mediaEntries.put(key, removed) notifyMediaDataLoaded(key, key, removed) - } else if (removed.active) { - // This player was still active - it didn't last long enough to time out: remove + } else if (removed.active && !isAbleToResume(removed)) { + // This player was still active - it didn't last long enough to time out, + // and its app doesn't normally support resume: remove if (DEBUG) Log.d(TAG, "Removing still-active player $key") notifyMediaDataRemoved(key) logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt index 92e0c851a462..b0389b50cd7d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt @@ -239,6 +239,8 @@ constructor( data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) if (data.resumeAction == null && !data.hasCheckedForResume && isEligibleForResume) { // TODO also check for a media button receiver intended for restarting (b/154127084) + // Set null action to prevent additional attempts to connect + mediaDataManager.setResumeAction(key, null) Log.d(TAG, "Checking for service component for " + data.packageName) val pm = context.packageManager val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE) @@ -249,9 +251,6 @@ constructor( backgroundExecutor.execute { tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName) } - } else { - // No service found - mediaDataManager.setResumeAction(key, null) } } } @@ -263,8 +262,6 @@ constructor( */ private fun tryUpdateResumptionList(key: String, componentName: ComponentName) { Log.d(TAG, "Testing if we can connect to $componentName") - // Set null action to prevent additional attempts to connect - mediaDataManager.setResumeAction(key, null) mediaBrowser = mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java index 3493b2453fd6..d460b5b5d782 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java @@ -85,16 +85,13 @@ public class ResumeMediaBrowser { * ResumeMediaBrowser#disconnect will be called automatically with this function. */ public void findRecentMedia() { - disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = mBrowserFactory.create( + MediaBrowser browser = mBrowserFactory.create( mComponentName, mConnectionCallback, rootHints); - updateMediaController(); - mLogger.logConnection(mComponentName, "findRecentMedia"); - mMediaBrowser.connect(); + connectBrowser(browser, "findRecentMedia"); } private final MediaBrowser.SubscriptionCallback mSubscriptionCallback = @@ -202,6 +199,21 @@ public class ResumeMediaBrowser { }; /** + * Connect using a new media browser. Disconnects the existing browser first, if it exists. + * @param browser media browser to connect + * @param reason Reason to log for connection + */ + private void connectBrowser(MediaBrowser browser, String reason) { + mLogger.logConnection(mComponentName, reason); + disconnect(); + mMediaBrowser = browser; + if (browser != null) { + browser.connect(); + } + updateMediaController(); + } + + /** * Disconnect the media browser. This should be done after callbacks have completed to * disconnect from the media browser service. */ @@ -222,10 +234,9 @@ public class ResumeMediaBrowser { * getting a media update from the app */ public void restart() { - disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = mBrowserFactory.create(mComponentName, + MediaBrowser browser = mBrowserFactory.create(mComponentName, new MediaBrowser.ConnectionCallback() { @Override public void onConnected() { @@ -265,9 +276,7 @@ public class ResumeMediaBrowser { disconnect(); } }, rootHints); - updateMediaController(); - mLogger.logConnection(mComponentName, "restart"); - mMediaBrowser.connect(); + connectBrowser(browser, "restart"); } @VisibleForTesting @@ -305,16 +314,13 @@ public class ResumeMediaBrowser { * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. */ public void testConnection() { - disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = mBrowserFactory.create( + MediaBrowser browser = mBrowserFactory.create( mComponentName, mConnectionCallback, rootHints); - updateMediaController(); - mLogger.logConnection(mComponentName, "testConnection"); - mMediaBrowser.connect(); + connectBrowser(browser, "testConnection"); } /** Updates mMediaController based on our current browser values. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 9d34df8c1eb8..938a9c35c205 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -51,6 +51,8 @@ import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.compose.ComposeFacade; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.plugins.qs.QS; import com.android.systemui.plugins.qs.QSContainerController; @@ -60,6 +62,7 @@ import com.android.systemui.qs.dagger.QSFragmentComponent; import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; @@ -112,6 +115,8 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private final MediaHost mQqsMediaHost; private final QSFragmentComponent.Factory mQsComponentFactory; private final QSFragmentDisableFlagsLogger mQsFragmentDisableFlagsLogger; + private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; + private final FeatureFlags mFeatureFlags; private final QSLogger mLogger; private final FooterActionsController mFooterActionsController; private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory; @@ -159,12 +164,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca // visible; private boolean mQsVisible; - /** - * Whether the notification panel uses the full width of the screen. - * - * Usually {@code true} on small screens, and {@code false} on large screens. - */ - private boolean mIsNotificationPanelFullWidth; + private boolean mIsSmallScreen; @Inject public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, @@ -176,13 +176,17 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca QSFragmentDisableFlagsLogger qsFragmentDisableFlagsLogger, DumpManager dumpManager, QSLogger qsLogger, FooterActionsController footerActionsController, - FooterActionsViewModel.Factory footerActionsViewModelFactory) { + FooterActionsViewModel.Factory footerActionsViewModelFactory, + LargeScreenShadeInterpolator largeScreenShadeInterpolator, + FeatureFlags featureFlags) { mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; mQsMediaHost = qsMediaHost; mQqsMediaHost = qqsMediaHost; mQsComponentFactory = qsComponentFactory; mQsFragmentDisableFlagsLogger = qsFragmentDisableFlagsLogger; mLogger = qsLogger; + mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; + mFeatureFlags = featureFlags; commandQueue.observe(getLifecycle(), this); mBypassController = keyguardBypassController; mStatusBarStateController = statusBarStateController; @@ -607,7 +611,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca @Override public void setIsNotificationPanelFullWidth(boolean isFullWidth) { - mIsNotificationPanelFullWidth = isFullWidth; + mIsSmallScreen = isFullWidth; } @Override @@ -710,7 +714,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } private float calculateAlphaProgress(float panelExpansionFraction) { - if (mIsNotificationPanelFullWidth) { + if (mIsSmallScreen) { // Small screens. QS alpha is not animated. return 1; } @@ -745,7 +749,12 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca // Alpha progress should be linear on lockscreen shade expansion. return progress; } - return ShadeInterpolation.getContentAlpha(progress); + if (mIsSmallScreen || !mFeatureFlags.isEnabled( + Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) { + return ShadeInterpolation.getContentAlpha(progress); + } else { + return mLargeScreenShadeInterpolator.getQsAlpha(progress); + } } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index c8c133774766..7cfe2327f992 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -57,7 +57,8 @@ import java.util.concurrent.Executor; import javax.inject.Inject; -class ImageExporter { +/** A class to help with exporting screenshot to storage. */ +public class ImageExporter { private static final String TAG = LogConfig.logTag(ImageExporter.class); static final Duration PENDING_ENTRY_TTL = Duration.ofHours(24); @@ -90,7 +91,7 @@ class ImageExporter { private final FeatureFlags mFlags; @Inject - ImageExporter(ContentResolver resolver, FeatureFlags flags) { + public ImageExporter(ContentResolver resolver, FeatureFlags flags) { mResolver = resolver; mFlags = flags; } @@ -148,7 +149,7 @@ class ImageExporter { * * @return a listenable future result */ - ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, + public ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, UserHandle owner) { return export(executor, requestId, bitmap, ZonedDateTime.now(), owner); } @@ -181,13 +182,14 @@ class ImageExporter { ); } - static class Result { - Uri uri; - UUID requestId; - String fileName; - long timestamp; - CompressFormat format; - boolean published; + /** The result returned by the task exporting screenshots to storage. */ + public static class Result { + public Uri uri; + public UUID requestId; + public String fileName; + public long timestamp; + public CompressFormat format; + public boolean published; @Override public String toString() { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 8721d71897f7..557e95c64443 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -419,6 +419,10 @@ public class ScreenshotController { return; } + mScreenBitmap = screenshot.getBitmap(); + String oldPackageName = mPackageName; + mPackageName = screenshot.getPackageNameString(); + if (!isUserSetupComplete(Process.myUserHandle())) { Log.w(TAG, "User setup not complete, displaying toast only"); // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing @@ -433,10 +437,6 @@ public class ScreenshotController { mScreenshotTakenInPortrait = mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; - String oldPackageName = mPackageName; - mPackageName = screenshot.getPackageNameString(); - - mScreenBitmap = screenshot.getBitmap(); // Optimizations mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java index fc94aed5336a..7a62bae5b5ae 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java @@ -93,13 +93,7 @@ public enum ScreenshotEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "User has discarded the result of a long screenshot") SCREENSHOT_LONG_SCREENSHOT_EXIT(911), @UiEvent(doc = "A screenshot has been taken and saved to work profile") - SCREENSHOT_SAVED_TO_WORK_PROFILE(1240), - @UiEvent(doc = "Notes application triggered the screenshot for notes") - SCREENSHOT_FOR_NOTE_TRIGGERED(1308), - @UiEvent(doc = "User accepted the screenshot to be sent to the notes app") - SCREENSHOT_FOR_NOTE_ACCEPTED(1309), - @UiEvent(doc = "User cancelled the screenshot for notes app flow") - SCREENSHOT_FOR_NOTE_CANCELLED(1310); + SCREENSHOT_SAVED_TO_WORK_PROFILE(1240); private final int mId; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 8323d1e6bae3..c44f4f391831 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -2883,7 +2883,10 @@ public final class NotificationPanelViewController implements Dumpable { mHeadsUpStartHeight = startHeight; float scrimMinFraction; if (mSplitShadeEnabled) { - boolean highHun = mHeadsUpStartHeight * 2.5 > mSplitShadeScrimTransitionDistance; + boolean highHun = mHeadsUpStartHeight * 2.5 + > + (mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION) + ? mSplitShadeFullTransitionDistance : mSplitShadeScrimTransitionDistance); // if HUN height is higher than 40% of predefined transition distance, it means HUN // is too high for regular transition. In that case we need to calculate transition // distance - here we take scrim transition distance as equal to shade transition @@ -4650,13 +4653,8 @@ public final class NotificationPanelViewController implements Dumpable { gesture possible. */ int pointerIndex = event.findPointerIndex(mTrackingPointer); if (pointerIndex < 0) { - if (mTrackingPointer < 0) { - pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); - } else { - mShadeLog.logMotionEvent(event, "Skipping intercept of multitouch pointer"); - return false; - } + pointerIndex = 0; + mTrackingPointer = event.getPointerId(pointerIndex); } final float x = event.getX(pointerIndex); final float y = event.getY(pointerIndex); diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenPortraitShadeInterpolator.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenPortraitShadeInterpolator.kt new file mode 100644 index 000000000000..05191317e86b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenPortraitShadeInterpolator.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.transition + +import android.util.MathUtils +import com.android.systemui.animation.ShadeInterpolation +import javax.inject.Inject + +/** Interpolator responsible for the shade when in portrait on a large screen. */ +internal class LargeScreenPortraitShadeInterpolator @Inject internal constructor() : + LargeScreenShadeInterpolator { + + override fun getBehindScrimAlpha(fraction: Float): Float { + return MathUtils.constrainedMap(0f, 1f, 0f, 0.3f, fraction) + } + + override fun getNotificationScrimAlpha(fraction: Float): Float { + return MathUtils.constrainedMap(0f, 1f, 0.3f, 0.75f, fraction) + } + + override fun getNotificationContentAlpha(fraction: Float): Float { + return ShadeInterpolation.getContentAlpha(fraction) + } + + override fun getNotificationFooterAlpha(fraction: Float): Float { + return ShadeInterpolation.getContentAlpha(fraction) + } + + override fun getQsAlpha(fraction: Float): Float { + return ShadeInterpolation.getContentAlpha(fraction) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolator.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolator.kt new file mode 100644 index 000000000000..671dfc9c80ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolator.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.transition + +/** An interpolator interface for the shade expansion. */ +interface LargeScreenShadeInterpolator { + + /** Returns the alpha for the behind/back scrim. */ + fun getBehindScrimAlpha(fraction: Float): Float + + /** Returns the alpha for the notification scrim. */ + fun getNotificationScrimAlpha(fraction: Float): Float + + /** Returns the alpha for the notifications. */ + fun getNotificationContentAlpha(fraction: Float): Float + + /** Returns the alpha for the notifications footer (Manager, Clear All). */ + fun getNotificationFooterAlpha(fraction: Float): Float + + /** Returns the alpha for the QS panel. */ + fun getQsAlpha(fraction: Float): Float +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolatorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolatorImpl.kt new file mode 100644 index 000000000000..fd57f21b2e1e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolatorImpl.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.transition + +import android.content.Context +import android.content.res.Configuration +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.LargeScreenUtils +import javax.inject.Inject + +/** Interpolator responsible for the shade when on large screens. */ +@SysUISingleton +internal class LargeScreenShadeInterpolatorImpl +@Inject +internal constructor( + configurationController: ConfigurationController, + private val context: Context, + private val splitShadeInterpolator: SplitShadeInterpolator, + private val portraitShadeInterpolator: LargeScreenPortraitShadeInterpolator, +) : LargeScreenShadeInterpolator { + + private var inSplitShade = false + + init { + configurationController.addCallback( + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateResources() + } + } + ) + updateResources() + } + + private fun updateResources() { + inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources) + } + + private val impl: LargeScreenShadeInterpolator + get() = + if (inSplitShade) { + splitShadeInterpolator + } else { + portraitShadeInterpolator + } + + override fun getBehindScrimAlpha(fraction: Float) = impl.getBehindScrimAlpha(fraction) + + override fun getNotificationScrimAlpha(fraction: Float) = + impl.getNotificationScrimAlpha(fraction) + + override fun getNotificationContentAlpha(fraction: Float) = + impl.getNotificationContentAlpha(fraction) + + override fun getNotificationFooterAlpha(fraction: Float) = + impl.getNotificationFooterAlpha(fraction) + + override fun getQsAlpha(fraction: Float) = impl.getQsAlpha(fraction) +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt index 218e897794fc..4e1c272ead99 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt @@ -23,6 +23,8 @@ import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.shade.PanelState import com.android.systemui.shade.STATE_OPENING import com.android.systemui.shade.ShadeExpansionChangeEvent @@ -45,7 +47,8 @@ constructor( private val scrimController: ScrimController, @Main private val resources: Resources, private val statusBarStateController: SysuiStatusBarStateController, - private val headsUpManager: HeadsUpManager + private val headsUpManager: HeadsUpManager, + private val featureFlags: FeatureFlags, ) { private var inSplitShade = false @@ -106,7 +109,8 @@ constructor( // in case of HUN we can't always use predefined distances to manage scrim // transition because dragDownPxAmount can start from value bigger than // splitShadeScrimTransitionDistance - !headsUpManager.isTrackingHeadsUp + !headsUpManager.isTrackingHeadsUp && + !featureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION) private fun isScreenUnlocked() = statusBarStateController.currentOrUpcomingState == StatusBarState.SHADE diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeInterpolator.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeInterpolator.kt new file mode 100644 index 000000000000..423ba8d4ec88 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/SplitShadeInterpolator.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.transition + +import android.util.MathUtils +import javax.inject.Inject + +/** Interpolator responsible for the split shade. */ +internal class SplitShadeInterpolator @Inject internal constructor() : + LargeScreenShadeInterpolator { + + override fun getBehindScrimAlpha(fraction: Float): Float { + // Start delay: 0% + // Duration: 40% + // End: 40% + return mapFraction(start = 0f, end = 0.4f, fraction) + } + + override fun getNotificationScrimAlpha(fraction: Float): Float { + // Start delay: 39% + // Duration: 27% + // End: 66% + return mapFraction(start = 0.39f, end = 0.66f, fraction) + } + + override fun getNotificationContentAlpha(fraction: Float): Float { + return getNotificationScrimAlpha(fraction) + } + + override fun getNotificationFooterAlpha(fraction: Float): Float { + // Start delay: 57.6% + // Duration: 32.1% + // End: 89.7% + return mapFraction(start = 0.576f, end = 0.897f, fraction) + } + + override fun getQsAlpha(fraction: Float): Float { + return getNotificationScrimAlpha(fraction) + } + + private fun mapFraction(start: Float, end: Float, fraction: Float) = + MathUtils.constrainedMap( + /* rangeMin= */ 0f, + /* rangeMax= */ 1f, + /* valueMin= */ start, + /* valueMax= */ end, + /* value= */ fraction + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 8f1e0a1a6b16..3709a139e57d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -39,7 +39,10 @@ import com.android.internal.policy.SystemBarUtils; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.ShadeInterpolation; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; @@ -216,7 +219,15 @@ public class NotificationShelf extends ActivatableNotificationView implements if (ambientState.isBouncerInTransit()) { viewState.setAlpha(aboutToShowBouncerProgress(expansion)); } else { - viewState.setAlpha(ShadeInterpolation.getContentAlpha(expansion)); + FeatureFlags flags = ambientState.getFeatureFlags(); + if (ambientState.isSmallScreen() || !flags.isEnabled( + Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) { + viewState.setAlpha(ShadeInterpolation.getContentAlpha(expansion)); + } else { + LargeScreenShadeInterpolator interpolator = + ambientState.getLargeScreenShadeInterpolator(); + viewState.setAlpha(interpolator.getNotificationContentAlpha(expansion)); + } } } else { viewState.setAlpha(1f - ambientState.getHideAmount()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index b0ad6a1ffbd8..37538a3ca93b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -500,8 +500,10 @@ constructor( private fun updateTextColorFromRegionSampler() { smartspaceViews.forEach { - val textColor = regionSamplers.getValue(it).currentForegroundColor() - it.setPrimaryTextColor(textColor) + val textColor = regionSamplers.get(it)?.currentForegroundColor() + if (textColor != null) { + it.setPrimaryTextColor(textColor) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index bbabde3f444e..1818dc562bb7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -153,7 +153,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // We don't correctly track dark mode until the content views are inflated, so always update // the background on first content update just in case it happens to be during a theme change. private boolean mUpdateSelfBackgroundOnUpdate = true; - private boolean mNotificationTranslationFinished = false; private boolean mIsSnoozed; private boolean mIsFaded; private boolean mAnimatePinnedRoundness = false; @@ -209,11 +208,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ private boolean mUserExpanded; /** - * Whether the blocking helper is showing on this notification (even if dismissed) - */ - private boolean mIsBlockingHelperShowing; - - /** * Has this notification been expanded while it was pinned */ private boolean mExpandedWhenPinned; @@ -1565,18 +1559,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - public void setBlockingHelperShowing(boolean isBlockingHelperShowing) { - mIsBlockingHelperShowing = isBlockingHelperShowing; - } - - public boolean isBlockingHelperShowing() { - return mIsBlockingHelperShowing; - } - - public boolean isBlockingHelperShowingAndTranslationFinished() { - return mIsBlockingHelperShowing && mNotificationTranslationFinished; - } - @Override public View getShelfTransformationTarget() { if (mIsSummaryWithChildren && !shouldShowPublic()) { @@ -2155,10 +2137,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public void setTranslation(float translationX) { invalidate(); - if (isBlockingHelperShowingAndTranslationFinished()) { - mGuts.setTranslationX(translationX); - return; - } else if (mDismissUsingRowTranslationX) { + if (mDismissUsingRowTranslationX) { setTranslationX(translationX); } else if (mTranslateableViews != null) { // Translate the group of views @@ -2186,10 +2165,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return getTranslationX(); } - if (isBlockingHelperShowingAndCanTranslate()) { - return mGuts.getTranslationX(); - } - if (mTranslateableViews != null && mTranslateableViews.size() > 0) { // All of the views in the list should have same translation, just use first one. return mTranslateableViews.get(0).getTranslationX(); @@ -2198,10 +2173,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return 0; } - private boolean isBlockingHelperShowingAndCanTranslate() { - return areGutsExposed() && mIsBlockingHelperShowing && mNotificationTranslationFinished; - } - public Animator getTranslateViewAnimator(final float leftTarget, AnimatorUpdateListener listener) { if (mTranslateAnim != null) { @@ -2223,9 +2194,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public void onAnimationEnd(Animator anim) { - if (mIsBlockingHelperShowing) { - mNotificationTranslationFinished = true; - } if (!cancelled && leftTarget == 0) { if (mMenuRow != null) { mMenuRow.resetMenu(); @@ -2805,9 +2773,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView int intrinsicBefore = getIntrinsicHeight(); mSensitive = sensitive; mSensitiveHiddenInGeneral = hideSensitive; - if (intrinsicBefore != getIntrinsicHeight()) { - // The animation has a few flaws and is highly visible, so jump cut instead. - notifyHeightChanged(false /* needsAnimation */); + int intrinsicAfter = getIntrinsicHeight(); + if (intrinsicBefore != intrinsicAfter) { + boolean needsAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM); + notifyHeightChanged(needsAnimation); } } @@ -2864,13 +2833,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView View[] publicViews = new View[]{mPublicLayout}; View[] hiddenChildren = showingPublic ? privateViews : publicViews; View[] shownChildren = showingPublic ? publicViews : privateViews; + // disappear/appear overlap: 10 percent of duration + long overlap = duration / 10; + // disappear duration: 1/3 of duration + half of overlap + long disappearDuration = duration / 3 + overlap / 2; + // appear duration: 2/3 of duration + half of overlap + long appearDuration = (duration - disappearDuration) + overlap / 2; for (final View hiddenView : hiddenChildren) { hiddenView.setVisibility(View.VISIBLE); hiddenView.animate().cancel(); hiddenView.animate() .alpha(0f) .setStartDelay(delay) - .setDuration(duration) + .setDuration(disappearDuration) .withEndAction(() -> { hiddenView.setVisibility(View.INVISIBLE); resetAllContentAlphas(); @@ -2882,8 +2857,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView showView.animate().cancel(); showView.animate() .alpha(1f) - .setStartDelay(delay) - .setDuration(duration); + .setStartDelay(delay + duration - appearDuration) + .setDuration(appearDuration); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java index 93f08123ab5a..596bdc09efe4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGuts.java @@ -238,12 +238,11 @@ public class NotificationGuts extends FrameLayout { } public void openControls( - boolean shouldDoCircularReveal, int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd) { - animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd); + animateOpen(x, y, onAnimationEnd); setExposed(true /* exposed */, needsFalsingProtection); } @@ -300,7 +299,7 @@ public class NotificationGuts extends FrameLayout { if (mGutsContent == null || !mGutsContent.handleCloseControls(save, force)) { // We only want to do a circular reveal if we're not showing the blocking helper. - animateClose(x, y, true /* shouldDoCircularReveal */); + animateClose(x, y); setExposed(false, mNeedsFalsingProtection); if (mClosedListener != null) { @@ -309,66 +308,45 @@ public class NotificationGuts extends FrameLayout { } } - /** Animates in the guts view via either a fade or a circular reveal. */ - private void animateOpen( - boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) { + /** Animates in the guts view with a circular reveal. */ + private void animateOpen(int x, int y, @Nullable Runnable onAnimationEnd) { if (isAttachedToWindow()) { - if (shouldDoCircularReveal) { - double horz = Math.max(getWidth() - x, x); - double vert = Math.max(getHeight() - y, y); - float r = (float) Math.hypot(horz, vert); - // Make sure we'll be visible after the circular reveal - setAlpha(1f); - // Circular reveal originating at (x, y) - Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r); - a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); - a.addListener(new AnimateOpenListener(onAnimationEnd)); - a.start(); - } else { - // Fade in content - this.setAlpha(0f); - this.animate() - .alpha(1f) - .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE) - .setInterpolator(Interpolators.ALPHA_IN) - .setListener(new AnimateOpenListener(onAnimationEnd)) - .start(); - } + double horz = Math.max(getWidth() - x, x); + double vert = Math.max(getHeight() - y, y); + float r = (float) Math.hypot(horz, vert); + // Make sure we'll be visible after the circular reveal + setAlpha(1f); + // Circular reveal originating at (x, y) + Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r); + a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); + a.addListener(new AnimateOpenListener(onAnimationEnd)); + a.start(); + } else { Log.w(TAG, "Failed to animate guts open"); } } - /** Animates out the guts view via either a fade or a circular reveal. */ + /** Animates out the guts view with a circular reveal. */ @VisibleForTesting - void animateClose(int x, int y, boolean shouldDoCircularReveal) { + void animateClose(int x, int y) { if (isAttachedToWindow()) { - if (shouldDoCircularReveal) { - // Circular reveal originating at (x, y) - if (x == -1 || y == -1) { - x = (getLeft() + getRight()) / 2; - y = (getTop() + getHeight() / 2); - } - double horz = Math.max(getWidth() - x, x); - double vert = Math.max(getHeight() - y, y); - float r = (float) Math.hypot(horz, vert); - Animator a = ViewAnimationUtils.createCircularReveal(this, - x, y, r, 0); - a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); - a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); - a.addListener(new AnimateCloseListener(this /* view */, mGutsContent)); - a.start(); - } else { - // Fade in the blocking helper. - this.animate() - .alpha(0f) - .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE) - .setInterpolator(Interpolators.ALPHA_OUT) - .setListener(new AnimateCloseListener(this, /* view */mGutsContent)) - .start(); + // Circular reveal originating at (x, y) + if (x == -1 || y == -1) { + x = (getLeft() + getRight()) / 2; + y = (getTop() + getHeight() / 2); } + double horz = Math.max(getWidth() - x, x); + double vert = Math.max(getHeight() - y, y); + float r = (float) Math.hypot(horz, vert); + Animator a = ViewAnimationUtils.createCircularReveal(this, + x, y, r, 0); + a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); + a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); + a.addListener(new AnimateCloseListener(this /* view */, mGutsContent)); + a.start(); } else { Log.w(TAG, "Failed to animate guts close"); mGutsContent.onFinishedClosing(); @@ -449,7 +427,7 @@ public class NotificationGuts extends FrameLayout { return mGutsContent != null && mGutsContent.isLeavebehind(); } - /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */ + /** Listener for animations executed in {@link #animateOpen(int, int, Runnable)}. */ private static class AnimateOpenListener extends AnimatorListenerAdapter { final Runnable mOnAnimationEnd; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 06d40803052e..46f1bb5ebd6f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -629,7 +629,6 @@ public class NotificationGutsManager implements NotifGutsViewManager { !mAccessibilityManager.isTouchExplorationEnabled()); guts.openControls( - !row.isBlockingHelperShowing(), x, y, needsFalsingProtection, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 6f4d6d944033..77ede0471603 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -29,6 +29,8 @@ 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.flags.FeatureFlags; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -55,6 +57,8 @@ public class AmbientState implements Dumpable { private final SectionProvider mSectionProvider; private final BypassController mBypassController; + private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; + private final FeatureFlags mFeatureFlags; /** * Used to read bouncer states. */ @@ -84,7 +88,7 @@ public class AmbientState implements Dumpable { private float mExpandingVelocity; private boolean mPanelTracking; private boolean mExpansionChanging; - private boolean mPanelFullWidth; + private boolean mIsSmallScreen; private boolean mPulsing; private boolean mUnlockHintRunning; private float mHideAmount; @@ -252,10 +256,14 @@ public class AmbientState implements Dumpable { @NonNull DumpManager dumpManager, @NonNull SectionProvider sectionProvider, @NonNull BypassController bypassController, - @Nullable StatusBarKeyguardViewManager statusBarKeyguardViewManager) { + @Nullable StatusBarKeyguardViewManager statusBarKeyguardViewManager, + @NonNull LargeScreenShadeInterpolator largeScreenShadeInterpolator, + @NonNull FeatureFlags featureFlags) { mSectionProvider = sectionProvider; mBypassController = bypassController; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; + mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; + mFeatureFlags = featureFlags; reload(context); dumpManager.registerDumpable(this); } @@ -574,12 +582,12 @@ public class AmbientState implements Dumpable { return mPanelTracking; } - public boolean isPanelFullWidth() { - return mPanelFullWidth; + public boolean isSmallScreen() { + return mIsSmallScreen; } - public void setPanelFullWidth(boolean panelFullWidth) { - mPanelFullWidth = panelFullWidth; + public void setSmallScreen(boolean smallScreen) { + mIsSmallScreen = smallScreen; } public void setUnlockHintRunning(boolean unlockHintRunning) { @@ -736,6 +744,14 @@ public class AmbientState implements Dumpable { return mIsClosing; } + public LargeScreenShadeInterpolator getLargeScreenShadeInterpolator() { + return mLargeScreenShadeInterpolator; + } + + public FeatureFlags getFeatureFlags() { + return mFeatureFlags; + } + @Override public void dump(PrintWriter pw, String[] args) { pw.println("mTopPadding=" + mTopPadding); @@ -751,7 +767,7 @@ public class AmbientState implements Dumpable { pw.println("mDimmed=" + mDimmed); pw.println("mStatusBarState=" + mStatusBarState); pw.println("mExpansionChanging=" + mExpansionChanging); - pw.println("mPanelFullWidth=" + mPanelFullWidth); + pw.println("mPanelFullWidth=" + mIsSmallScreen); pw.println("mPulsing=" + mPulsing); pw.println("mPulseHeight=" + mPulseHeight); pw.println("mTrackedHeadsUpRow.key=" + logKey(mTrackedHeadsUpRow)); 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 977e1bb31049..47d8f48feba5 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 @@ -5223,7 +5223,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) public void setIsFullWidth(boolean isFullWidth) { - mAmbientState.setPanelFullWidth(isFullWidth); + mAmbientState.setSmallScreen(isFullWidth); } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) 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 14b0763580e9..02621fe4c103 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 @@ -70,7 +70,6 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEv import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeController; -import com.android.systemui.shade.transition.ShadeTransitionController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener; @@ -171,7 +170,6 @@ public class NotificationStackScrollLayoutController { private final CentralSurfaces mCentralSurfaces; private final SectionHeaderController mSilentHeaderController; private final LockscreenShadeTransitionController mLockscreenShadeTransitionController; - private final ShadeTransitionController mShadeTransitionController; private final InteractionJankMonitor mJankMonitor; private final NotificationStackSizeCalculator mNotificationStackSizeCalculator; private final StackStateLogger mStackStateLogger; @@ -662,7 +660,6 @@ public class NotificationStackScrollLayoutController { NotifPipelineFlags notifPipelineFlags, NotifCollection notifCollection, LockscreenShadeTransitionController lockscreenShadeTransitionController, - ShadeTransitionController shadeTransitionController, UiEventLogger uiEventLogger, NotificationRemoteInputManager remoteInputManager, VisibilityLocationProviderDelegator visibilityLocationProviderDelegator, @@ -694,7 +691,6 @@ public class NotificationStackScrollLayoutController { mMetricsLogger = metricsLogger; mDumpManager = dumpManager; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; - mShadeTransitionController = shadeTransitionController; mFalsingCollector = falsingCollector; mFalsingManager = falsingManager; mResources = resources; @@ -777,7 +773,6 @@ public class NotificationStackScrollLayoutController { mScrimController.setScrimBehindChangeRunnable(mView::updateBackgroundDimming); mLockscreenShadeTransitionController.setStackScroller(this); - mShadeTransitionController.setNotificationStackScrollLayoutController(this); mLockscreenUserManager.addUserChangedListener(mLockscreenUserChangeListener); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index a425792f6523..5516edeac344 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -29,6 +29,9 @@ import com.android.internal.policy.SystemBarUtils; import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.R; import com.android.systemui.animation.ShadeInterpolation; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.LegacySourceType; @@ -135,7 +138,6 @@ public class StackScrollAlgorithm { AmbientState ambientState) { for (ExpandableView view : algorithmState.visibleChildren) { final ViewState viewState = view.getViewState(); - final boolean isHunGoingToShade = ambientState.isShadeExpanded() && view == ambientState.getTrackedHeadsUpRow(); @@ -148,9 +150,14 @@ public class StackScrollAlgorithm { } else if (ambientState.isExpansionChanging()) { // Adjust alpha for shade open & close. float expansion = ambientState.getExpansionFraction(); - viewState.setAlpha(ambientState.isBouncerInTransit() - ? BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion) - : ShadeInterpolation.getContentAlpha(expansion)); + if (ambientState.isBouncerInTransit()) { + viewState.setAlpha( + BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion)); + } else if (view instanceof FooterView) { + viewState.setAlpha(interpolateFooterAlpha(ambientState)); + } else { + viewState.setAlpha(interpolateNotificationContentAlpha(ambientState)); + } } // For EmptyShadeView if on keyguard, we need to control the alpha to create @@ -182,6 +189,28 @@ public class StackScrollAlgorithm { } } + private float interpolateFooterAlpha(AmbientState ambientState) { + float expansion = ambientState.getExpansionFraction(); + FeatureFlags flags = ambientState.getFeatureFlags(); + if (ambientState.isSmallScreen() + || !flags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) { + return ShadeInterpolation.getContentAlpha(expansion); + } + LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator(); + return interpolator.getNotificationFooterAlpha(expansion); + } + + private float interpolateNotificationContentAlpha(AmbientState ambientState) { + float expansion = ambientState.getExpansionFraction(); + FeatureFlags flags = ambientState.getFeatureFlags(); + if (ambientState.isSmallScreen() + || !flags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) { + return ShadeInterpolation.getContentAlpha(expansion); + } + LargeScreenShadeInterpolator interpolator = ambientState.getLargeScreenShadeInterpolator(); + return interpolator.getNotificationContentAlpha(expansion); + } + /** * How expanded or collapsed notifications are when pulling down the shade. * 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 b5d51ce2b9ee..dcd219ad94b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -3571,7 +3571,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { boolean goingToSleepWithoutAnimation = isGoingToSleep() && !mDozeParameters.shouldControlScreenOff(); boolean disabled = (!mDeviceInteractive && !mDozeServiceHost.isPulsing()) - || goingToSleepWithoutAnimation; + || goingToSleepWithoutAnimation + || mDeviceProvisionedController.isFrpActive(); mNotificationPanelViewController.setTouchAndAnimationDisabled(disabled); mNotificationIconAreaController.setAnimationsEnabled(!disabled); } @@ -3745,10 +3746,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { boolean launchingAffordanceWithPreview = mLaunchingAffordance; mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview); - if (mAlternateBouncerInteractor.isVisibleState()) { - if (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED - || mTransitionToFullShadeProgress > 0f) { + if ((!isOccluded() || isPanelExpanded()) + && (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED + || mTransitionToFullShadeProgress > 0f)) { mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE); } else { mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED); 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 8fd967594198..f5d2eee35c93 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -54,15 +54,18 @@ import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; +import com.android.systemui.keyguard.shared.model.ScrimAlpha; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.NotificationPanelViewController; -import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.notification.stack.ViewState; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -206,7 +209,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final ScreenOffAnimationController mScreenOffAnimationController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private final SysuiStatusBarStateController mStatusBarStateController; private GradientColors mColors; private boolean mNeedsDrawableColorUpdate; @@ -245,6 +247,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private boolean mWallpaperVisibilityTimedOut; private int mScrimsVisibility; private final TriConsumer<ScrimState, Float, GradientColors> mScrimStateListener; + private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; + private final FeatureFlags mFeatureFlags; private Consumer<Integer> mScrimVisibleListener; private boolean mBlankScreen; private boolean mScreenBlankingCallbackCalled; @@ -265,12 +269,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private CoroutineDispatcher mMainDispatcher; private boolean mIsBouncerToGoneTransitionRunning = false; private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; - private final Consumer<Float> mScrimAlphaConsumer = - (Float alpha) -> { + private final Consumer<ScrimAlpha> mScrimAlphaConsumer = + (ScrimAlpha alphas) -> { + mInFrontAlpha = alphas.getFrontAlpha(); mScrimInFront.setViewAlpha(mInFrontAlpha); + + mNotificationsAlpha = alphas.getNotificationsAlpha(); mNotificationsScrim.setViewAlpha(mNotificationsAlpha); - mBehindAlpha = alpha; - mScrimBehind.setViewAlpha(alpha); + + mBehindAlpha = alphas.getBehindAlpha(); + mScrimBehind.setViewAlpha(mBehindAlpha); }; Consumer<TransitionStep> mPrimaryBouncerToGoneTransition; @@ -292,15 +300,17 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump StatusBarKeyguardViewManager statusBarKeyguardViewManager, PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, KeyguardTransitionInteractor keyguardTransitionInteractor, - SysuiStatusBarStateController sysuiStatusBarStateController, - @Main CoroutineDispatcher mainDispatcher) { + @Main CoroutineDispatcher mainDispatcher, + LargeScreenShadeInterpolator largeScreenShadeInterpolator, + FeatureFlags featureFlags) { mScrimStateListener = lightBarController::setScrimState; + mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; + mFeatureFlags = featureFlags; mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; mKeyguardStateController = keyguardStateController; mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen(); mKeyguardUpdateMonitor = keyguardUpdateMonitor; - mStatusBarStateController = sysuiStatusBarStateController; mKeyguardVisibilityCallback = new KeyguardVisibilityCallback(); mHandler = handler; mMainExecutor = mainExecutor; @@ -400,7 +410,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump collectFlow(behindScrim, mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition(), mPrimaryBouncerToGoneTransition, mMainDispatcher); - collectFlow(behindScrim, mPrimaryBouncerToGoneTransitionViewModel.getScrimBehindAlpha(), + collectFlow(behindScrim, mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha(), mScrimAlphaConsumer, mMainDispatcher); } @@ -837,16 +847,21 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump if (!mScreenOffAnimationController.shouldExpandNotifications() && !mAnimatingPanelExpansionOnUnlock && !occluding) { - if (mClipsQsScrim) { + if (mTransparentScrimBackground) { + mBehindAlpha = 0; + mNotificationsAlpha = 0; + } else if (mClipsQsScrim) { float behindFraction = getInterpolatedFraction(); behindFraction = (float) Math.pow(behindFraction, 0.8f); - mBehindAlpha = mTransparentScrimBackground ? 0 : 1; - mNotificationsAlpha = - mTransparentScrimBackground ? 0 : behindFraction * mDefaultScrimAlpha; + mBehindAlpha = 1; + mNotificationsAlpha = behindFraction * mDefaultScrimAlpha; } else { - if (mTransparentScrimBackground) { - mBehindAlpha = 0; - mNotificationsAlpha = 0; + if (mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) { + mBehindAlpha = mLargeScreenShadeInterpolator.getBehindScrimAlpha( + mPanelExpansionFraction * mDefaultScrimAlpha); + mNotificationsAlpha = + mLargeScreenShadeInterpolator.getNotificationScrimAlpha( + mPanelExpansionFraction); } else { // Behind scrim will finish fading in at 30% expansion. float behindFraction = MathUtils @@ -1100,8 +1115,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mBehindAlpha = 1; } // Prevent notification scrim flicker when transitioning away from keyguard. - if (mKeyguardStateController.isKeyguardGoingAway() - && !mStatusBarStateController.leaveOpenOnKeyguardHide()) { + if (mKeyguardStateController.isKeyguardGoingAway()) { mNotificationsAlpha = 0; } 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 3c32131220d7..b6a3ba80da15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -733,7 +733,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } else { showBouncerOrKeyguard(hideBouncerWhenShowing); } - hideAlternateBouncer(false); + if (hideBouncerWhenShowing) { + hideAlternateBouncer(false); + } mKeyguardUpdateManager.sendKeyguardReset(); updateStates(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 078a00d33493..189f2e3098fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -27,6 +27,7 @@ import android.app.PendingIntent; import android.app.TaskStackBuilder; import android.content.Context; import android.content.Intent; +import android.content.pm.ResolveInfo; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; @@ -45,6 +46,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.NotificationVisibility; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.EventLogTags; @@ -74,6 +76,7 @@ import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.wmshell.BubblesManager; +import java.util.List; import java.util.Optional; import java.util.concurrent.Executor; @@ -81,6 +84,7 @@ import javax.inject.Inject; import dagger.Lazy; + /** * Status bar implementation of {@link NotificationActivityStarter}. */ @@ -572,16 +576,29 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte }); // not immersive & a fullscreen alert should be shown - final PendingIntent fullscreenIntent = + final PendingIntent fullScreenIntent = entry.getSbn().getNotification().fullScreenIntent; - mLogger.logSendingFullScreenIntent(entry, fullscreenIntent); + mLogger.logSendingFullScreenIntent(entry, fullScreenIntent); try { EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, entry.getKey()); mCentralSurfaces.wakeUpForFullScreenIntent(); - fullscreenIntent.send(); + fullScreenIntent.send(); entry.notifyFullScreenIntentLaunched(); mMetricsLogger.count("note_fullscreen", 1); + + String activityName; + List<ResolveInfo> resolveInfos = fullScreenIntent.queryIntentComponents(0); + if (resolveInfos.size() > 0 && resolveInfos.get(0) != null + && resolveInfos.get(0).activityInfo != null + && resolveInfos.get(0).activityInfo.name != null) { + activityName = resolveInfos.get(0).activityInfo.name; + } else { + activityName = ""; + } + FrameworkStatsLog.write(FrameworkStatsLog.FULL_SCREEN_INTENT_LAUNCHED, + fullScreenIntent.getCreatorUid(), + activityName); } catch (PendingIntent.CanceledException e) { // ignore } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt index 159f689de370..4156fc152602 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLogger.kt @@ -51,8 +51,8 @@ constructor( ) } - fun logOnLost(network: Network) { - LoggerHelper.logOnLost(buffer, TAG, network) + fun logOnLost(network: Network, isDefaultNetworkCallback: Boolean) { + LoggerHelper.logOnLost(buffer, TAG, network, isDefaultNetworkCallback) } fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt index 85729c12cd4c..19f0242040fd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/DataConnectionState.kt @@ -24,9 +24,11 @@ import android.telephony.TelephonyManager.DATA_HANDOVER_IN_PROGRESS import android.telephony.TelephonyManager.DATA_SUSPENDED import android.telephony.TelephonyManager.DATA_UNKNOWN import android.telephony.TelephonyManager.DataState +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger /** Internal enum representation of the telephony data connection states */ -enum class DataConnectionState { +enum class DataConnectionState : Diffable<DataConnectionState> { Connected, Connecting, Disconnected, @@ -34,7 +36,17 @@ enum class DataConnectionState { Suspended, HandoverInProgress, Unknown, - Invalid, + Invalid; + + override fun logDiffs(prevVal: DataConnectionState, row: TableRowLogger) { + if (prevVal != this) { + row.logChange(COL_CONNECTION_STATE, name) + } + } + + companion object { + private const val COL_CONNECTION_STATE = "connectionState" + } } fun @receiver:DataState Int.toDataConnectionType(): DataConnectionState = 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 deleted file mode 100644 index ed7f60b50bb9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt +++ /dev/null @@ -1,176 +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.mobile.data.model - -import android.annotation.IntRange -import android.telephony.CellSignalStrength -import android.telephony.TelephonyCallback.CarrierNetworkListener -import android.telephony.TelephonyCallback.DataActivityListener -import android.telephony.TelephonyCallback.DataConnectionStateListener -import android.telephony.TelephonyCallback.DisplayInfoListener -import android.telephony.TelephonyCallback.ServiceStateListener -import android.telephony.TelephonyCallback.SignalStrengthsListener -import android.telephony.TelephonyDisplayInfo -import android.telephony.TelephonyManager -import androidx.annotation.VisibleForTesting -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 - * a Subscription in the telephony world. These models are the result of a single telephony listener - * which has many callbacks which each modify some particular field on this object. - * - * The design goal here is to de-normalize fields from the system into our model fields below. So - * any new field that needs to be tracked should be copied into this data class rather than - * threading complex system objects through the pipeline. - */ -data class MobileConnectionModel( - /** 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, - - /** - * TODO (b/263167683): Clarify this field - * - * This check comes from [com.android.settingslib.Utils.isInService]. It is intended to be a - * mapping from a ServiceState to a notion of connectivity. Notably, it will consider a - * connection to be in-service if either the voice registration state is IN_SERVICE or the data - * registration state is IN_SERVICE and NOT IWLAN. - */ - val isInService: Boolean = false, - - /** 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, - - /** Fields below from [DataConnectionStateListener.onDataConnectionStateChanged] */ - val dataConnectionState: DataConnectionState = Disconnected, - - /** - * Fields below from [DataActivityListener.onDataActivity]. See [TelephonyManager] for the - * values - */ - val dataActivityDirection: DataActivityModel = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ), - - /** Fields below from [CarrierNetworkListener.onCarrierNetworkChange] */ - val carrierNetworkChangeActive: Boolean = false, - - /** Fields below 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.name) - } - - 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.isInService != isInService) { - row.logChange(COL_IS_IN_SERVICE, isInService) - } - - 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.hasActivityIn != dataActivityDirection.hasActivityIn) { - row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn) - } - - if (prevVal.dataActivityDirection.hasActivityOut != dataActivityDirection.hasActivityOut) { - row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut) - } - - 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.name) - row.logChange(COL_EMERGENCY, isEmergencyOnly) - row.logChange(COL_ROAMING, isRoaming) - row.logChange(COL_OPERATOR, operatorAlphaShort) - row.logChange(COL_IS_IN_SERVICE, isInService) - row.logChange(COL_IS_GSM, isGsm) - row.logChange(COL_CDMA_LEVEL, cdmaLevel) - row.logChange(COL_PRIMARY_LEVEL, primaryLevel) - row.logChange(COL_ACTIVITY_DIRECTION_IN, dataActivityDirection.hasActivityIn) - row.logChange(COL_ACTIVITY_DIRECTION_OUT, dataActivityDirection.hasActivityOut) - row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive) - row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString()) - } - - @VisibleForTesting - companion object { - const val COL_EMERGENCY = "EmergencyOnly" - const val COL_ROAMING = "Roaming" - const val COL_OPERATOR = "OperatorName" - const val COL_IS_IN_SERVICE = "IsInService" - 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_IN = "DataActivity.In" - const val COL_ACTIVITY_DIRECTION_OUT = "DataActivity.Out" - 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/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt index 5562e73f0478..cf7a313a4cb1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt @@ -17,8 +17,12 @@ package com.android.systemui.statusbar.pipeline.mobile.data.model import android.telephony.Annotation.NetworkType +import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy /** @@ -26,11 +30,19 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy * on whether or not the display info contains an override type, we may have to call different * methods on [MobileMappingsProxy] to generate an icon lookup key. */ -sealed interface ResolvedNetworkType { +sealed interface ResolvedNetworkType : Diffable<ResolvedNetworkType> { val lookupKey: String + override fun logDiffs(prevVal: ResolvedNetworkType, row: TableRowLogger) { + if (prevVal != this) { + row.logChange(COL_NETWORK_TYPE, this.toString()) + } + } + object UnknownNetworkType : ResolvedNetworkType { - override val lookupKey: String = "unknown" + override val lookupKey: String = MobileMappings.toIconKey(NETWORK_TYPE_UNKNOWN) + + override fun toString(): String = "Unknown" } data class DefaultNetworkType( @@ -47,5 +59,11 @@ sealed interface ResolvedNetworkType { override val lookupKey: String = "cwf" val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI + + override fun toString(): String = "CarrierMerged" + } + + companion object { + private const val COL_NETWORK_TYPE = "networkType" } } 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 6187f64e011d..90c32dc08045 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 @@ -17,11 +17,12 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo -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.DataConnectionState 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.shared.data.model.DataActivityModel import kotlinx.coroutines.flow.StateFlow /** @@ -45,11 +46,57 @@ interface MobileConnectionRepository { */ val tableLogBuffer: TableLogBuffer + /** True if the [android.telephony.ServiceState] says this connection is emergency calls only */ + val isEmergencyOnly: StateFlow<Boolean> + + /** True if [android.telephony.ServiceState] says we are roaming */ + val isRoaming: StateFlow<Boolean> + + /** + * 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: StateFlow<String?> + + /** + * TODO (b/263167683): Clarify this field + * + * This check comes from [com.android.settingslib.Utils.isInService]. It is intended to be a + * mapping from a ServiceState to a notion of connectivity. Notably, it will consider a + * connection to be in-service if either the voice registration state is IN_SERVICE or the data + * registration state is IN_SERVICE and NOT IWLAN. + */ + val isInService: StateFlow<Boolean> + + /** True if [android.telephony.SignalStrength] told us that this connection is using GSM */ + val isGsm: StateFlow<Boolean> + + /** + * There is still specific logic in the pipeline that calls out CDMA level explicitly. This + * field is not completely orthogonal to [primaryLevel], because CDMA could be primary. + */ + // @IntRange(from = 0, to = 4) + val cdmaLevel: StateFlow<Int> + + /** [android.telephony.SignalStrength]'s concept of the overall signal level */ + // @IntRange(from = 0, to = 4) + val primaryLevel: StateFlow<Int> + + /** The current data connection state. See [DataConnectionState] */ + val dataConnectionState: StateFlow<DataConnectionState> + + /** The current data activity direction. See [DataActivityModel] */ + val dataActivityDirection: StateFlow<DataActivityModel> + + /** True if there is currently a carrier network change in process */ + val carrierNetworkChangeActive: StateFlow<Boolean> + /** - * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single - * listener + model. + * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or + * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon */ - val connectionInfo: StateFlow<MobileConnectionModel> + val resolvedNetworkType: StateFlow<ResolvedNetworkType> /** The total number of levels. Used with [SignalDrawable]. */ val numberOfLevels: StateFlow<Int> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt new file mode 100644 index 000000000000..809772eec2f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo + +import android.telephony.CellSignalStrength +import android.telephony.TelephonyManager +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +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.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_CARRIER_NETWORK_CHANGE +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_CDMA_LEVEL +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_GSM +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_IN_SERVICE +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn + +/** + * Demo version of [MobileConnectionRepository]. Note that this class shares all of its flows using + * [SharingStarted.WhileSubscribed()] to give the same semantics as using a regular + * [MutableStateFlow] while still logging all of the inputs in the same manor as the production + * repos. + */ +class DemoMobileConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, + val scope: CoroutineScope, +) : MobileConnectionRepository { + private val _isEmergencyOnly = MutableStateFlow(false) + override val isEmergencyOnly = + _isEmergencyOnly + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_EMERGENCY, + _isEmergencyOnly.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isEmergencyOnly.value) + + private val _isRoaming = MutableStateFlow(false) + override val isRoaming = + _isRoaming + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_ROAMING, + _isRoaming.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isRoaming.value) + + private val _operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null) + override val operatorAlphaShort = + _operatorAlphaShort + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_OPERATOR, + _operatorAlphaShort.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _operatorAlphaShort.value) + + private val _isInService = MutableStateFlow(false) + override val isInService = + _isInService + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_IN_SERVICE, + _isInService.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value) + + private val _isGsm = MutableStateFlow(false) + override val isGsm = + _isGsm + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_GSM, + _isGsm.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isGsm.value) + + private val _cdmaLevel = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val cdmaLevel = + _cdmaLevel + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_CDMA_LEVEL, + _cdmaLevel.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _cdmaLevel.value) + + private val _primaryLevel = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val primaryLevel = + _primaryLevel + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_PRIMARY_LEVEL, + _primaryLevel.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _primaryLevel.value) + + private val _dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected) + override val dataConnectionState = + _dataConnectionState + .logDiffsForTable(tableLogBuffer, columnPrefix = "", _dataConnectionState.value) + .stateIn(scope, SharingStarted.WhileSubscribed(), _dataConnectionState.value) + + private val _dataActivityDirection = + MutableStateFlow( + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ) + ) + override val dataActivityDirection = + _dataActivityDirection + .logDiffsForTable(tableLogBuffer, columnPrefix = "", _dataActivityDirection.value) + .stateIn(scope, SharingStarted.WhileSubscribed(), _dataActivityDirection.value) + + private val _carrierNetworkChangeActive = MutableStateFlow(false) + override val carrierNetworkChangeActive = + _carrierNetworkChangeActive + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_CARRIER_NETWORK_CHANGE, + _carrierNetworkChangeActive.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierNetworkChangeActive.value) + + private val _resolvedNetworkType: MutableStateFlow<ResolvedNetworkType> = + MutableStateFlow(ResolvedNetworkType.UnknownNetworkType) + override val resolvedNetworkType = + _resolvedNetworkType + .logDiffsForTable(tableLogBuffer, columnPrefix = "", _resolvedNetworkType.value) + .stateIn(scope, SharingStarted.WhileSubscribed(), _resolvedNetworkType.value) + + override val numberOfLevels = MutableStateFlow(MobileConnectionRepository.DEFAULT_NUM_LEVELS) + + override val dataEnabled = MutableStateFlow(true) + + override val cdmaRoaming = MutableStateFlow(false) + + override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo network")) + + /** + * Process a new demo mobile event. Note that [resolvedNetworkType] must be passed in separately + * from the event, due to the requirement to reverse the mobile mappings lookup in the top-level + * repository. + */ + fun processDemoMobileEvent( + event: FakeNetworkEventModel.Mobile, + resolvedNetworkType: ResolvedNetworkType, + ) { + // This is always true here, because we split out disabled states at the data-source level + dataEnabled.value = true + networkName.value = NetworkNameModel.IntentDerived(event.name) + + cdmaRoaming.value = event.roaming + _isRoaming.value = event.roaming + // TODO(b/261029387): not yet supported + _isEmergencyOnly.value = false + _operatorAlphaShort.value = event.name + _isInService.value = (event.level ?: 0) > 0 + // TODO(b/261029387): not yet supported + _isGsm.value = false + _cdmaLevel.value = event.level ?: 0 + _primaryLevel.value = event.level ?: 0 + // TODO(b/261029387): not yet supported + _dataConnectionState.value = DataConnectionState.Connected + _dataActivityDirection.value = + (event.activity ?: TelephonyManager.DATA_ACTIVITY_NONE).toMobileDataActivityModel() + _carrierNetworkChangeActive.value = event.carrierNetworkChange + _resolvedNetworkType.value = resolvedNetworkType + } + + fun processCarrierMergedEvent(event: FakeWifiEventModel.CarrierMerged) { + // This is always true here, because we split out disabled states at the data-source level + dataEnabled.value = true + networkName.value = NetworkNameModel.IntentDerived(CARRIER_MERGED_NAME) + numberOfLevels.value = event.numberOfLevels + cdmaRoaming.value = false + _primaryLevel.value = event.level + _cdmaLevel.value = event.level + _dataActivityDirection.value = event.activity.toMobileDataActivityModel() + + // These fields are always the same for carrier-merged networks + _resolvedNetworkType.value = ResolvedNetworkType.CarrierMergedNetworkType + _dataConnectionState.value = DataConnectionState.Connected + _isRoaming.value = false + _isEmergencyOnly.value = false + _operatorAlphaShort.value = null + _isInService.value = true + _isGsm.value = false + _carrierNetworkChangeActive.value = false + } + + companion object { + private const val CARRIER_MERGED_NAME = "Carrier Merged Network" + } +} 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 e92483232186..3cafb7377260 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,30 +18,22 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo 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 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository 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.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE -import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import javax.inject.Inject @@ -183,7 +175,7 @@ constructor( private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer { val tableLogBuffer = logFactory.getOrCreate( - "DemoMobileConnectionLog [$subId]", + "DemoMobileConnectionLog[$subId]", MOBILE_CONNECTION_BUFFER_SIZE, ) @@ -191,6 +183,7 @@ constructor( DemoMobileConnectionRepository( subId, tableLogBuffer, + scope, ) return CacheContainer(repo, lastMobileState = null) } @@ -237,23 +230,18 @@ constructor( } } - private fun processEnabledMobileState(state: Mobile) { + private fun processEnabledMobileState(event: Mobile) { // get or create the connection repo, and set its values - val subId = state.subId ?: DEFAULT_SUB_ID + val subId = event.subId ?: DEFAULT_SUB_ID maybeCreateSubscription(subId) val connection = getRepoForSubId(subId) - connectionRepoCache[subId]?.lastMobileState = state + connectionRepoCache[subId]?.lastMobileState = event // TODO(b/261029387): until we have a command, use the most recent subId defaultDataSubId.value = subId - // This is always true here, because we split out disabled states at the data-source level - connection.dataEnabled.value = true - connection.networkName.value = NetworkNameModel.IntentDerived(state.name) - - connection.cdmaRoaming.value = state.roaming - connection.connectionInfo.value = state.toMobileConnectionModel() + connection.processDemoMobileEvent(event, event.dataType.toResolvedNetworkType()) } private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { @@ -272,13 +260,7 @@ constructor( defaultDataSubId.value = subId val connection = getRepoForSubId(subId) - // This is always true here, because we split out disabled states at the data-source level - connection.dataEnabled.value = true - connection.networkName.value = NetworkNameModel.IntentDerived(CARRIER_MERGED_NAME) - connection.numberOfLevels.value = event.numberOfLevels - connection.cdmaRoaming.value = false - connection.connectionInfo.value = event.toMobileConnectionModel() - Log.e("CCS", "output connection info = ${connection.connectionInfo.value}") + connection.processCarrierMergedEvent(event) } private fun maybeRemoveSubscription(subId: Int?) { @@ -332,29 +314,6 @@ constructor( private fun subIdsString(): String = _subscriptions.value.joinToString(",") { it.subscriptionId.toString() } - private fun Mobile.toMobileConnectionModel(): MobileConnectionModel { - return MobileConnectionModel( - isEmergencyOnly = false, // TODO(b/261029387): not yet supported - isRoaming = roaming, - isInService = (level ?: 0) > 0, - 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 ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel(), - carrierNetworkChangeActive = carrierNetworkChange, - resolvedNetworkType = dataType.toResolvedNetworkType() - ) - } - - private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel { - return createCarrierMergedConnectionModel( - this.level, - activity.toMobileDataActivityModel(), - ) - } - private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType { val key = mobileMappingsReverseLookup.value[this] ?: "dis" return DefaultNetworkType(key) @@ -364,8 +323,6 @@ constructor( private const val TAG = "DemoMobileConnectionsRepo" private const val DEFAULT_SUB_ID = 1 - - private const val CARRIER_MERGED_NAME = "Carrier Merged Network" } } @@ -374,18 +331,3 @@ class CacheContainer( /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */ var lastMobileState: Mobile?, ) - -class DemoMobileConnectionRepository( - override val subId: Int, - override val tableLogBuffer: TableLogBuffer, -) : MobileConnectionRepository { - override val connectionInfo = MutableStateFlow(MobileConnectionModel()) - - override val numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS) - - override val dataEnabled = MutableStateFlow(true) - - override val cdmaRoaming = MutableStateFlow(false) - - override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo network")) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt index 8f6a87b089f2..94d6d0b1db44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -16,18 +16,17 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod +import android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN import android.telephony.TelephonyManager import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application 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 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS -import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import javax.inject.Inject @@ -94,16 +93,6 @@ class CarrierMergedConnectionRepository( } } - override val connectionInfo: StateFlow<MobileConnectionModel> = - combine(network, wifiRepository.wifiActivity) { network, activity -> - if (network == null) { - MobileConnectionModel() - } else { - createCarrierMergedConnectionModel(network.level, activity) - } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel()) - override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(ROAMING).asStateFlow() override val networkName: StateFlow<NetworkNameModel> = @@ -129,34 +118,54 @@ class CarrierMergedConnectionRepository( } .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS) - override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled + override val primaryLevel = + network + .map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN } + .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) - companion object { - /** - * Creates an instance of [MobileConnectionModel] that represents a carrier merged network - * with the given [level] and [activity]. - */ - fun createCarrierMergedConnectionModel( - level: Int, - activity: DataActivityModel, - ): MobileConnectionModel { - return MobileConnectionModel( - primaryLevel = level, - cdmaLevel = level, - dataActivityDirection = activity, - // Here and below: These values are always the same for every carrier-merged - // connection. - resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, - dataConnectionState = DataConnectionState.Connected, - isRoaming = ROAMING, - isEmergencyOnly = false, - operatorAlphaShort = null, - isInService = true, - isGsm = false, - carrierNetworkChangeActive = false, + override val cdmaLevel = + network + .map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN } + .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + + override val dataActivityDirection = wifiRepository.wifiActivity + + override val resolvedNetworkType = + network + .map { + if (it != null) { + ResolvedNetworkType.CarrierMergedNetworkType + } else { + ResolvedNetworkType.UnknownNetworkType + } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + ResolvedNetworkType.UnknownNetworkType ) - } + override val dataConnectionState = + network + .map { + if (it != null) { + DataConnectionState.Connected + } else { + DataConnectionState.Disconnected + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), DataConnectionState.Disconnected) + + override val isRoaming = MutableStateFlow(false).asStateFlow() + override val isEmergencyOnly = MutableStateFlow(false).asStateFlow() + override val operatorAlphaShort = MutableStateFlow(null).asStateFlow() + override val isInService = MutableStateFlow(true).asStateFlow() + override val isGsm = MutableStateFlow(false).asStateFlow() + override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow() + + override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled + + companion object { // Carrier merged is never roaming private const val ROAMING = false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt index a39ea0abce5a..b3737ecd1e0b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -114,15 +114,147 @@ class FullMobileConnectionRepository( .flatMapLatest { it.cdmaRoaming } .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value) - override val connectionInfo = + override val isEmergencyOnly = activeRepo - .flatMapLatest { it.connectionInfo } + .flatMapLatest { it.isEmergencyOnly } .logDiffsForTable( tableLogBuffer, columnPrefix = "", - initialValue = activeRepo.value.connectionInfo.value, + columnName = COL_EMERGENCY, + activeRepo.value.isEmergencyOnly.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.isEmergencyOnly.value + ) + + override val isRoaming = + activeRepo + .flatMapLatest { it.isRoaming } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_ROAMING, + activeRepo.value.isRoaming.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isRoaming.value) + + override val operatorAlphaShort = + activeRepo + .flatMapLatest { it.operatorAlphaShort } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_OPERATOR, + activeRepo.value.operatorAlphaShort.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.operatorAlphaShort.value + ) + + override val isInService = + activeRepo + .flatMapLatest { it.isInService } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_IN_SERVICE, + activeRepo.value.isInService.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value) + + override val isGsm = + activeRepo + .flatMapLatest { it.isGsm } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_GSM, + activeRepo.value.isGsm.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isGsm.value) + + override val cdmaLevel = + activeRepo + .flatMapLatest { it.cdmaLevel } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_CDMA_LEVEL, + activeRepo.value.cdmaLevel.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaLevel.value) + + override val primaryLevel = + activeRepo + .flatMapLatest { it.primaryLevel } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_PRIMARY_LEVEL, + activeRepo.value.primaryLevel.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.primaryLevel.value) + + override val dataConnectionState = + activeRepo + .flatMapLatest { it.dataConnectionState } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + activeRepo.value.dataConnectionState.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.dataConnectionState.value + ) + + override val dataActivityDirection = + activeRepo + .flatMapLatest { it.dataActivityDirection } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + activeRepo.value.dataActivityDirection.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.dataActivityDirection.value + ) + + override val carrierNetworkChangeActive = + activeRepo + .flatMapLatest { it.carrierNetworkChangeActive } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_CARRIER_NETWORK_CHANGE, + activeRepo.value.carrierNetworkChangeActive.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.carrierNetworkChangeActive.value + ) + + override val resolvedNetworkType = + activeRepo + .flatMapLatest { it.resolvedNetworkType } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + activeRepo.value.resolvedNetworkType.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.resolvedNetworkType.value ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value) override val dataEnabled = activeRepo @@ -187,4 +319,15 @@ class FullMobileConnectionRepository( fun tableBufferLogName(subId: Int): String = "MobileConnectionLog[$subId]" } } + + companion object { + const val COL_EMERGENCY = "emergencyOnly" + const val COL_ROAMING = "roaming" + const val COL_OPERATOR = "operatorName" + const val COL_IS_IN_SERVICE = "isInService" + const val COL_IS_GSM = "isGsm" + const val COL_CDMA_LEVEL = "cdmaLevel" + const val COL_PRIMARY_LEVEL = "primaryLevel" + const val COL_CARRIER_NETWORK_CHANGE = "carrierNetworkChangeActive" + } } 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 e182bc66081a..f1fc3868d690 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 @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod import android.content.Context import android.content.IntentFilter import android.telephony.CellSignalStrength +import android.telephony.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength @@ -37,7 +38,7 @@ 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.statusbar.pipeline.mobile.data.MobileInputLogger -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected 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 @@ -49,6 +50,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierCon import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -62,10 +64,10 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn @@ -165,83 +167,100 @@ class MobileConnectionRepositoryImpl( } .shareIn(scope, SharingStarted.WhileSubscribed()) - private fun updateConnectionState( - prevState: MobileConnectionModel, - callbackEvent: CallbackEvent, - ): MobileConnectionModel = - when (callbackEvent) { - is CallbackEvent.OnServiceStateChanged -> { - val serviceState = callbackEvent.serviceState - prevState.copy( - isEmergencyOnly = serviceState.isEmergencyOnly, - isRoaming = serviceState.roaming, - operatorAlphaShort = serviceState.operatorAlphaShort, - isInService = Utils.isInService(serviceState), - ) - } - is CallbackEvent.OnSignalStrengthChanged -> { - val signalStrength = callbackEvent.signalStrength - val cdmaLevel = - signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java).let { - strengths -> - if (!strengths.isEmpty()) { - strengths[0].level - } else { - CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN - } - } + override val isEmergencyOnly = + callbackEvents + .filterIsInstance<CallbackEvent.OnServiceStateChanged>() + .map { it.serviceState.isEmergencyOnly } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) - val primaryLevel = signalStrength.level + override val isRoaming = + callbackEvents + .filterIsInstance<CallbackEvent.OnServiceStateChanged>() + .map { it.serviceState.roaming } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) - prevState.copy( - cdmaLevel = cdmaLevel, - primaryLevel = primaryLevel, - isGsm = signalStrength.isGsm, - ) - } - is CallbackEvent.OnDataConnectionStateChanged -> { - prevState.copy(dataConnectionState = callbackEvent.dataState.toDataConnectionType()) - } - is CallbackEvent.OnDataActivity -> { - prevState.copy( - dataActivityDirection = callbackEvent.direction.toMobileDataActivityModel() - ) - } - is CallbackEvent.OnCarrierNetworkChange -> { - prevState.copy(carrierNetworkChangeActive = callbackEvent.active) - } - is CallbackEvent.OnDisplayInfoChanged -> { - val telephonyDisplayInfo = callbackEvent.telephonyDisplayInfo - val networkType = - if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) { - UnknownNetworkType - } else if ( - telephonyDisplayInfo.overrideNetworkType == OVERRIDE_NETWORK_TYPE_NONE - ) { - DefaultNetworkType( - mobileMappingsProxy.toIconKey(telephonyDisplayInfo.networkType) - ) + override val operatorAlphaShort = + callbackEvents + .filterIsInstance<CallbackEvent.OnServiceStateChanged>() + .map { it.serviceState.operatorAlphaShort } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val isInService = + callbackEvents + .filterIsInstance<CallbackEvent.OnServiceStateChanged>() + .map { Utils.isInService(it.serviceState) } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val isGsm = + callbackEvents + .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>() + .map { it.signalStrength.isGsm } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val cdmaLevel = + callbackEvents + .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>() + .map { + it.signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java).let { + strengths -> + if (strengths.isNotEmpty()) { + strengths[0].level } else { - OverrideNetworkType( - mobileMappingsProxy.toIconKeyOverride( - telephonyDisplayInfo.overrideNetworkType - ) - ) + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN } - prevState.copy(resolvedNetworkType = networkType) - } - is CallbackEvent.OnDataEnabledChanged -> { - // Not part of this object, handled in a separate flow - prevState + } } - } + .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) - override val connectionInfo = run { - val initial = MobileConnectionModel() + override val primaryLevel = callbackEvents - .scan(initial, ::updateConnectionState) - .stateIn(scope, SharingStarted.WhileSubscribed(), initial) - } + .filterIsInstance<CallbackEvent.OnSignalStrengthChanged>() + .map { it.signalStrength.level } + .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + + override val dataConnectionState = + callbackEvents + .filterIsInstance<CallbackEvent.OnDataConnectionStateChanged>() + .map { it.dataState.toDataConnectionType() } + .stateIn(scope, SharingStarted.WhileSubscribed(), Disconnected) + + override val dataActivityDirection = + callbackEvents + .filterIsInstance<CallbackEvent.OnDataActivity>() + .map { it.direction.toMobileDataActivityModel() } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + DataActivityModel(hasActivityIn = false, hasActivityOut = false) + ) + + override val carrierNetworkChangeActive = + callbackEvents + .filterIsInstance<CallbackEvent.OnCarrierNetworkChange>() + .map { it.active } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val resolvedNetworkType = + callbackEvents + .filterIsInstance<CallbackEvent.OnDisplayInfoChanged>() + .map { + if (it.telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) { + UnknownNetworkType + } else if ( + it.telephonyDisplayInfo.overrideNetworkType == OVERRIDE_NETWORK_TYPE_NONE + ) { + DefaultNetworkType( + mobileMappingsProxy.toIconKey(it.telephonyDisplayInfo.networkType) + ) + } else { + OverrideNetworkType( + mobileMappingsProxy.toIconKeyOverride( + it.telephonyDisplayInfo.overrideNetworkType + ) + ) + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), UnknownNetworkType) override val numberOfLevels = systemUiCarrierConfig.shouldInflateSignalStrength 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 f97e41c018f4..b7da3f27c70a 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 @@ -266,6 +266,7 @@ constructor( val callback = object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { override fun onLost(network: Network) { + logger.logOnLost(network, isDefaultNetworkCallback = true) // Send a disconnected model when lost. Maybe should create a sealed // type or null here? trySend(MobileConnectivityModel()) @@ -275,6 +276,11 @@ constructor( network: Network, caps: NetworkCapabilities ) { + logger.logOnCapabilitiesChanged( + network, + caps, + isDefaultNetworkCallback = true, + ) trySend( MobileConnectivityModel( isConnected = caps.hasTransport(TRANSPORT_CELLULAR), 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 4caf2b09a3f2..7df6764fda1a 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 @@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn @@ -133,11 +134,9 @@ class MobileIconInteractorImpl( override val isForceHidden: Flow<Boolean>, connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { - private val connectionInfo = connectionRepository.connectionInfo - override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer - override val activity = connectionInfo.mapLatest { it.dataActivityDirection } + override val activity = connectionRepository.dataActivityDirection override val isConnected: Flow<Boolean> = defaultMobileConnectivity.mapLatest { it.isConnected } @@ -155,11 +154,11 @@ class MobileIconInteractorImpl( override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled override val networkName = - combine(connectionInfo, connectionRepository.networkName) { connection, networkName -> - if ( - networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null - ) { - NetworkNameModel.IntentDerived(connection.operatorAlphaShort) + combine(connectionRepository.operatorAlphaShort, connectionRepository.networkName) { + operatorAlphaShort, + networkName -> + if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) { + NetworkNameModel.IntentDerived(operatorAlphaShort) } else { networkName } @@ -173,19 +172,19 @@ class MobileIconInteractorImpl( /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ override val networkTypeIconGroup: StateFlow<MobileIconGroup> = combine( - connectionInfo, + connectionRepository.resolvedNetworkType, defaultMobileIconMapping, defaultMobileIconGroup, isDefault, - ) { info, mapping, defaultGroup, isDefault -> + ) { resolvedNetworkType, mapping, defaultGroup, isDefault -> if (!isDefault) { return@combine NOT_DEFAULT_DATA } - when (info.resolvedNetworkType) { + when (resolvedNetworkType) { is ResolvedNetworkType.CarrierMergedNetworkType -> - info.resolvedNetworkType.iconGroupOverride - else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup + resolvedNetworkType.iconGroupOverride + else -> mapping[resolvedNetworkType.lookupKey] ?: defaultGroup } } .distinctUntilChanged() @@ -200,17 +199,19 @@ class MobileIconInteractorImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) - override val isEmergencyOnly: StateFlow<Boolean> = - connectionInfo - .mapLatest { it.isEmergencyOnly } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isEmergencyOnly = connectionRepository.isEmergencyOnly override val isRoaming: StateFlow<Boolean> = - combine(connectionInfo, connectionRepository.cdmaRoaming) { connection, cdmaRoaming -> - if (connection.carrierNetworkChangeActive) { + combine( + connectionRepository.carrierNetworkChangeActive, + connectionRepository.isGsm, + connectionRepository.isRoaming, + connectionRepository.cdmaRoaming, + ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming -> + if (carrierNetworkChangeActive) { false - } else if (connection.isGsm) { - connection.isRoaming + } else if (isGsm) { + isRoaming } else { cdmaRoaming } @@ -218,12 +219,17 @@ class MobileIconInteractorImpl( .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val level: StateFlow<Int> = - combine(connectionInfo, alwaysUseCdmaLevel) { connection, alwaysUseCdmaLevel -> + combine( + connectionRepository.isGsm, + connectionRepository.primaryLevel, + connectionRepository.cdmaLevel, + alwaysUseCdmaLevel, + ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel -> when { // GSM connections should never use the CDMA level - connection.isGsm -> connection.primaryLevel - alwaysUseCdmaLevel -> connection.cdmaLevel - else -> connection.primaryLevel + isGsm -> primaryLevel + alwaysUseCdmaLevel -> cdmaLevel + else -> primaryLevel } } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) @@ -236,12 +242,9 @@ class MobileIconInteractorImpl( ) override val isDataConnected: StateFlow<Boolean> = - connectionInfo - .mapLatest { connection -> connection.dataConnectionState == Connected } + connectionRepository.dataConnectionState + .map { it == Connected } .stateIn(scope, SharingStarted.WhileSubscribed(), false) - override val isInService = - connectionRepository.connectionInfo - .mapLatest { it.isInService } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isInService = connectionRepository.isInService } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt index 6f29e33b5a17..a96e8ff20dd4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/LoggerHelper.kt @@ -38,11 +38,24 @@ object LoggerHelper { int1 = network.getNetId() str1 = networkCapabilities.toString() }, - { "onCapabilitiesChanged[default=$bool1]: net=$int1 capabilities=$str1" } + { "on${if (bool1) "Default" else ""}CapabilitiesChanged: net=$int1 capabilities=$str1" } ) } - fun logOnLost(buffer: LogBuffer, tag: String, network: Network) { - buffer.log(tag, LogLevel.INFO, { int1 = network.getNetId() }, { "onLost: net=$int1" }) + fun logOnLost( + buffer: LogBuffer, + tag: String, + network: Network, + isDefaultNetworkCallback: Boolean, + ) { + buffer.log( + tag, + LogLevel.INFO, + { + int1 = network.getNetId() + bool1 = isDefaultNetworkCallback + }, + { "on${if (bool1) "Default" else ""}Lost: net=$int1" } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index ee58160a7d3b..b5e7b7a13505 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -128,6 +128,7 @@ constructor( } override fun onLost(network: Network) { + logger.logOnLost(network, isDefaultNetworkCallback = true) // The system no longer has a default network, so wifi is definitely not // default. trySend(false) @@ -179,7 +180,7 @@ constructor( } override fun onLost(network: Network) { - logger.logOnLost(network) + logger.logOnLost(network, isDefaultNetworkCallback = false) wifiNetworkChangeEvents.tryEmit(Unit) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt index a32e47592355..bb0b166f7aba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiInputLogger.kt @@ -48,8 +48,8 @@ constructor( ) } - fun logOnLost(network: Network) { - LoggerHelper.logOnLost(buffer, TAG, network) + fun logOnLost(network: Network, isDefaultNetworkCallback: Boolean) { + LoggerHelper.logOnLost(buffer, TAG, network, isDefaultNetworkCallback) } fun logIntent(intentName: String) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java index 3944c8c77f49..0d09fc184892 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java @@ -21,7 +21,8 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceP /** * Controller to cache in process the state of the device provisioning. * <p> - * This controller keeps track of the values of device provisioning and user setup complete + * This controller keeps track of the values of device provisioning, user setup complete, and + * whether Factory Reset Protection is active. */ public interface DeviceProvisionedController extends CallbackController<DeviceProvisionedListener> { @@ -49,6 +50,9 @@ public interface DeviceProvisionedController extends CallbackController<DevicePr */ boolean isCurrentUserSetup(); + /** Returns true when Factory Reset Protection is locking the device. */ + boolean isFrpActive(); + /** * Interface to provide calls when the values tracked change */ @@ -69,5 +73,10 @@ public interface DeviceProvisionedController extends CallbackController<DevicePr * Call when some user changes from not provisioned to provisioned */ default void onUserSetupChanged() { } + + /** + * Called when the state of FRP changes. + */ + default void onFrpActiveChanged() {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt index a6b7d9c56900..32c64f457501 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt @@ -60,9 +60,11 @@ open class DeviceProvisionedControllerImpl @Inject constructor( } private val deviceProvisionedUri = globalSettings.getUriFor(Settings.Global.DEVICE_PROVISIONED) + private val frpActiveUri = secureSettings.getUriFor(Settings.Secure.SECURE_FRP_MODE) private val userSetupUri = secureSettings.getUriFor(Settings.Secure.USER_SETUP_COMPLETE) private val deviceProvisioned = AtomicBoolean(false) + private val frpActive = AtomicBoolean(false) @GuardedBy("lock") private val userSetupComplete = SparseBooleanArray() @GuardedBy("lock") @@ -89,11 +91,15 @@ open class DeviceProvisionedControllerImpl @Inject constructor( userId: Int ) { val updateDeviceProvisioned = deviceProvisionedUri in uris + val updateFrp = frpActiveUri in uris val updateUser = if (userSetupUri in uris) userId else NO_USERS - updateValues(updateDeviceProvisioned, updateUser) + updateValues(updateDeviceProvisioned, updateFrp, updateUser) if (updateDeviceProvisioned) { onDeviceProvisionedChanged() } + if (updateFrp) { + onFrpActiveChanged() + } if (updateUser != NO_USERS) { onUserSetupChanged() } @@ -103,7 +109,7 @@ open class DeviceProvisionedControllerImpl @Inject constructor( private val userChangedCallback = object : UserTracker.Callback { @WorkerThread override fun onUserChanged(newUser: Int, userContext: Context) { - updateValues(updateDeviceProvisioned = false, updateUser = newUser) + updateValues(updateDeviceProvisioned = false, updateFrp = false, updateUser = newUser) onUserSwitched() } @@ -125,19 +131,27 @@ open class DeviceProvisionedControllerImpl @Inject constructor( updateValues() userTracker.addCallback(userChangedCallback, backgroundExecutor) globalSettings.registerContentObserver(deviceProvisionedUri, observer) + globalSettings.registerContentObserver(frpActiveUri, observer) secureSettings.registerContentObserverForUser(userSetupUri, observer, UserHandle.USER_ALL) } @WorkerThread - private fun updateValues(updateDeviceProvisioned: Boolean = true, updateUser: Int = ALL_USERS) { + private fun updateValues( + updateDeviceProvisioned: Boolean = true, + updateFrp: Boolean = true, + updateUser: Int = ALL_USERS + ) { if (updateDeviceProvisioned) { deviceProvisioned .set(globalSettings.getInt(Settings.Global.DEVICE_PROVISIONED, 0) != 0) } + if (updateFrp) { + frpActive.set(globalSettings.getInt(Settings.Secure.SECURE_FRP_MODE, 0) != 0) + } synchronized(lock) { if (updateUser == ALL_USERS) { - val N = userSetupComplete.size() - for (i in 0 until N) { + val n = userSetupComplete.size() + for (i in 0 until n) { val user = userSetupComplete.keyAt(i) val value = secureSettings .getIntForUser(Settings.Secure.USER_SETUP_COMPLETE, 0, user) != 0 @@ -172,6 +186,10 @@ open class DeviceProvisionedControllerImpl @Inject constructor( return deviceProvisioned.get() } + override fun isFrpActive(): Boolean { + return frpActive.get() + } + override fun isUserSetup(user: Int): Boolean { val index = synchronized(lock) { userSetupComplete.indexOfKey(user) @@ -196,7 +214,13 @@ open class DeviceProvisionedControllerImpl @Inject constructor( override fun onDeviceProvisionedChanged() { dispatchChange( - DeviceProvisionedController.DeviceProvisionedListener::onDeviceProvisionedChanged + DeviceProvisionedController.DeviceProvisionedListener::onDeviceProvisionedChanged + ) + } + + override fun onFrpActiveChanged() { + dispatchChange( + DeviceProvisionedController.DeviceProvisionedListener::onFrpActiveChanged ) } @@ -221,6 +245,7 @@ open class DeviceProvisionedControllerImpl @Inject constructor( override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println("Device provisioned: ${deviceProvisioned.get()}") + pw.println("Factory Reset Protection active: ${frpActive.get()}") synchronized(lock) { pw.println("User setup complete: $userSetupComplete") pw.println("Listeners: $listeners") diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 3f895ad0b5b4..02d447976746 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -32,6 +32,8 @@ import android.os.UserManager import android.provider.Settings import android.util.Log import com.android.internal.util.UserIcons +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.R import com.android.systemui.SystemUISecondaryUserService import com.android.systemui.animation.Expandable @@ -90,6 +92,7 @@ constructor( @Application private val applicationScope: CoroutineScope, telephonyInteractor: TelephonyInteractor, broadcastDispatcher: BroadcastDispatcher, + keyguardUpdateMonitor: KeyguardUpdateMonitor, @Background private val backgroundDispatcher: CoroutineDispatcher, private val activityManager: ActivityManager, private val refreshUsersScheduler: RefreshUsersScheduler, @@ -286,6 +289,12 @@ constructor( val isSimpleUserSwitcher: Boolean get() = repository.isSimpleUserSwitcher() + val keyguardUpdateMonitorCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onKeyguardGoingAway() { + dismissDialog() + } + } init { refreshUsersScheduler.refreshIfNotPaused() @@ -316,6 +325,7 @@ constructor( onBroadcastReceived(intent, previousSelectedUser) } .launchIn(applicationScope) + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) } fun addCallback(callback: UserCallback) { diff --git a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java index 064c224b0568..c6da55c1e20a 100644 --- a/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java +++ b/packages/SystemUI/src/com/android/systemui/util/service/ObservableServiceConnection.java @@ -119,11 +119,11 @@ public class ObservableServiceConnection<T> implements ServiceConnection { /** * Default constructor for {@link ObservableServiceConnection}. - * @param context The context from which the service will be bound with. + * @param context The context from which the service will be bound with. * @param serviceIntent The intent to bind service with. - * @param executor The executor for connection callbacks to be delivered on - * @param transformer A {@link ServiceTransformer} for transforming the resulting service - * into a desired type. + * @param executor The executor for connection callbacks to be delivered on + * @param transformer A {@link ServiceTransformer} for transforming the resulting service + * into a desired type. */ @Inject public ObservableServiceConnection(Context context, Intent serviceIntent, @@ -143,7 +143,13 @@ public class ObservableServiceConnection<T> implements ServiceConnection { * @return {@code true} if initiating binding succeed, {@code false} otherwise. */ public boolean bind() { - final boolean bindResult = mContext.bindService(mServiceIntent, mFlags, mExecutor, this); + boolean bindResult = false; + try { + bindResult = mContext.bindService(mServiceIntent, mFlags, mExecutor, this); + } catch (SecurityException e) { + Log.d(TAG, "Could not bind to service", e); + mContext.unbindService(this); + } mBoundCalled = true; if (DEBUG) { Log.d(TAG, "bind. bound:" + bindResult); @@ -197,7 +203,7 @@ public class ObservableServiceConnection<T> implements ServiceConnection { Log.d(TAG, "removeCallback:" + callback); } - mExecutor.execute(()-> mCallbacks.removeIf(el -> el.get() == callback)); + mExecutor.execute(() -> mCallbacks.removeIf(el-> el.get() == callback)); } private void onDisconnected(@DisconnectReason int reason) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index b847d67b448e..1cfcf8cbbdcb 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1473,6 +1473,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, mHandler.removeMessages(H.SHOW); if (mIsAnimatingDismiss) { Log.d(TAG, "dismissH: isAnimatingDismiss"); + Trace.endSection(); return; } mIsAnimatingDismiss = true; diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index bbc7bc92e819..4dc4c2cbf93c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -519,6 +519,38 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { } @Test + public void testWillRunDismissFromKeyguardIsTrue() { + ActivityStarter.OnDismissAction action = mock(ActivityStarter.OnDismissAction.class); + when(action.willRunAnimationOnKeyguard()).thenReturn(true); + mKeyguardSecurityContainerController.setOnDismissAction(action, null /* cancelAction */); + + mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */); + + assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isTrue(); + } + + @Test + public void testWillRunDismissFromKeyguardIsFalse() { + ActivityStarter.OnDismissAction action = mock(ActivityStarter.OnDismissAction.class); + when(action.willRunAnimationOnKeyguard()).thenReturn(false); + mKeyguardSecurityContainerController.setOnDismissAction(action, null /* cancelAction */); + + mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */); + + assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isFalse(); + } + + @Test + public void testWillRunDismissFromKeyguardIsFalseWhenNoDismissActionSet() { + mKeyguardSecurityContainerController.setOnDismissAction(null /* action */, + null /* cancelAction */); + + mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */); + + assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isFalse(); + } + + @Test public void testOnStartingToHide() { mKeyguardSecurityContainerController.onStartingToHide(); verify(mInputViewController).onStartingToHide(); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 531006da8210..565fc57e81b7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -263,9 +263,6 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { assertThat(viewFlipperConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); assertThat(userSwitcherConstraint.layout.topToTop).isEqualTo(PARENT_ID); assertThat(userSwitcherConstraint.layout.bottomToBottom).isEqualTo(PARENT_ID); - assertThat(userSwitcherConstraint.layout.bottomMargin).isEqualTo( - getContext().getResources().getDimensionPixelSize( - R.dimen.bouncer_user_switcher_y_trans)); assertThat(viewFlipperConstraint.layout.horizontalChainStyle).isEqualTo(CHAIN_SPREAD); assertThat(userSwitcherConstraint.layout.horizontalChainStyle).isEqualTo(CHAIN_SPREAD); assertThat(viewFlipperConstraint.layout.mHeight).isEqualTo(MATCH_CONSTRAINT); diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt index f01da2dd3a6b..8a5c5b58d058 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt @@ -61,7 +61,7 @@ class FontInterpolatorTest : SysuiTestCase() { val interp = FontInterpolator() assertSameAxes(startFont, interp.lerp(startFont, endFont, 0f)) assertSameAxes(endFont, interp.lerp(startFont, endFont, 1f)) - assertSameAxes("'wght' 500, 'ital' 0.5, 'GRAD' 450", interp.lerp(startFont, endFont, 0.5f)) + assertSameAxes("'wght' 496, 'ital' 0.5, 'GRAD' 450", interp.lerp(startFont, endFont, 0.5f)) } @Test @@ -74,7 +74,7 @@ class FontInterpolatorTest : SysuiTestCase() { .build() val interp = FontInterpolator() - assertSameAxes("'wght' 250, 'ital' 0.5", interp.lerp(startFont, endFont, 0.5f)) + assertSameAxes("'wght' 249, 'ital' 0.5", interp.lerp(startFont, endFont, 0.5f)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt index 35cd3d261562..10bfc1b292d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsListingControllerImplTest.kt @@ -45,6 +45,8 @@ import com.android.systemui.util.mockito.argThat import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -62,7 +64,6 @@ import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import java.util.concurrent.Executor @SmallTest @RunWith(AndroidTestingRunner::class) @@ -106,6 +107,7 @@ class ControlsListingControllerImplTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) `when`(userTracker.userId).thenReturn(user) + `when`(userTracker.userHandle).thenReturn(UserHandle.of(user)) `when`(userTracker.userContext).thenReturn(context) // Return disabled by default `when`(packageManager.getComponentEnabledSetting(any())) @@ -564,6 +566,79 @@ class ControlsListingControllerImplTest : SysuiTestCase() { assertTrue(controller.getCurrentServices().isEmpty()) } + @Test + fun testForceReloadQueriesPackageManager() { + val user = 10 + `when`(userTracker.userHandle).thenReturn(UserHandle.of(user)) + + controller.forceReload() + verify(packageManager).queryIntentServicesAsUser( + argThat(IntentMatcherAction(ControlsProviderService.SERVICE_CONTROLS)), + argThat(FlagsMatcher( + PackageManager.GET_META_DATA.toLong() or + PackageManager.GET_SERVICES.toLong() or + PackageManager.MATCH_DIRECT_BOOT_AWARE.toLong() or + PackageManager.MATCH_DIRECT_BOOT_UNAWARE.toLong() + )), + eq(UserHandle.of(user)) + ) + } + + @Test + fun testForceReloadUpdatesList() { + val resolveInfo = ResolveInfo() + resolveInfo.serviceInfo = ServiceInfo(componentName) + + `when`(packageManager.queryIntentServicesAsUser( + argThat(IntentMatcherAction(ControlsProviderService.SERVICE_CONTROLS)), + argThat(FlagsMatcher( + PackageManager.GET_META_DATA.toLong() or + PackageManager.GET_SERVICES.toLong() or + PackageManager.MATCH_DIRECT_BOOT_AWARE.toLong() or + PackageManager.MATCH_DIRECT_BOOT_UNAWARE.toLong() + )), + any<UserHandle>() + )).thenReturn(listOf(resolveInfo)) + + controller.forceReload() + + val services = controller.getCurrentServices() + assertThat(services.size).isEqualTo(1) + assertThat(services[0].serviceInfo.componentName).isEqualTo(componentName) + } + + @Test + fun testForceReloadCallsListeners() { + controller.addCallback(mockCallback) + executor.runAllReady() + + @Suppress("unchecked_cast") + val captor: ArgumentCaptor<List<ControlsServiceInfo>> = + ArgumentCaptor.forClass(List::class.java) + as ArgumentCaptor<List<ControlsServiceInfo>> + + val resolveInfo = ResolveInfo() + resolveInfo.serviceInfo = ServiceInfo(componentName) + + `when`(packageManager.queryIntentServicesAsUser( + any(), + any<PackageManager.ResolveInfoFlags>(), + any<UserHandle>() + )).thenReturn(listOf(resolveInfo)) + + reset(mockCallback) + controller.forceReload() + + verify(mockCallback).onServicesUpdated(capture(captor)) + + val services = captor.value + + assertThat(services.size).isEqualTo(1) + assertThat(services[0].serviceInfo.componentName).isEqualTo(componentName) + } + + + private fun ServiceInfo( componentName: ComponentName, panelActivityComponentName: ComponentName? = null @@ -600,7 +675,7 @@ class ControlsListingControllerImplTest : SysuiTestCase() { private fun setUpQueryResult(infos: List<ActivityInfo>) { `when`( packageManager.queryIntentActivitiesAsUser( - argThat(IntentMatcher(activityName)), + argThat(IntentMatcherComponent(activityName)), argThat(FlagsMatcher(FLAGS)), eq(UserHandle.of(user)) ) @@ -609,7 +684,7 @@ class ControlsListingControllerImplTest : SysuiTestCase() { }) } - private class IntentMatcher( + private class IntentMatcherComponent( private val componentName: ComponentName ) : ArgumentMatcher<Intent> { override fun matches(argument: Intent?): Boolean { @@ -617,6 +692,14 @@ class ControlsListingControllerImplTest : SysuiTestCase() { } } + private class IntentMatcherAction( + private val action: String + ) : ArgumentMatcher<Intent> { + override fun matches(argument: Intent?): Boolean { + return argument?.action == action + } + } + private class FlagsMatcher( private val flags: Long ) : ArgumentMatcher<PackageManager.ResolveInfoFlags> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt index 9d8084d4f2f4..bd7e98e16b91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/start/ControlsStartableTest.kt @@ -42,6 +42,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions @@ -80,9 +82,10 @@ class ControlsStartableTest : SysuiTestCase() { fun testNoPreferredPackagesNoDefaultSelected_noNewSelection() { `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController, never()).setPreferredSelection(any()) } @@ -92,9 +95,10 @@ class ControlsStartableTest : SysuiTestCase() { whenever(authorizedPanelsRepository.getPreferredPackages()) .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) - `when`(controlsListingController.getCurrentServices()).thenReturn(emptyList()) + setUpControlsListingControls(emptyList()) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController, never()).setPreferredSelection(any()) } @@ -105,9 +109,10 @@ class ControlsStartableTest : SysuiTestCase() { .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "not panel", hasPanel = false)) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController, never()).setPreferredSelection(any()) } @@ -119,9 +124,10 @@ class ControlsStartableTest : SysuiTestCase() { `when`(controlsController.getPreferredSelection()) .thenReturn(mock<SelectedItem.PanelItem>()) val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController, never()).setPreferredSelection(any()) } @@ -132,9 +138,10 @@ class ControlsStartableTest : SysuiTestCase() { .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController).setPreferredSelection(listings[0].toPanelItem()) } @@ -149,9 +156,10 @@ class ControlsStartableTest : SysuiTestCase() { ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true), ControlsServiceInfo(ComponentName("other_package", "cls"), "non panel", false) ) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController).setPreferredSelection(listings[0].toPanelItem()) } @@ -166,9 +174,10 @@ class ControlsStartableTest : SysuiTestCase() { ControlsServiceInfo(ComponentName("other_package", "cls"), "panel", true), ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true) ) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController).setPreferredSelection(listings[1].toPanelItem()) } @@ -176,10 +185,11 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testPreferredSelectionIsPanel_bindOnStart() { val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) `when`(controlsController.getPreferredSelection()).thenReturn(listings[0].toPanelItem()) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController).bindComponentForPanel(TEST_COMPONENT_PANEL) } @@ -187,11 +197,12 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testPreferredSelectionPanel_listingNoPanel_notBind() { val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false)) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) `when`(controlsController.getPreferredSelection()) .thenReturn(SelectedItem.PanelItem("panel", TEST_COMPONENT_PANEL)) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController, never()).bindComponentForPanel(any()) } @@ -199,10 +210,11 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testNotPanelSelection_noBind() { val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = false)) - `when`(controlsListingController.getCurrentServices()).thenReturn(listings) + setUpControlsListingControls(listings) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) createStartable(enabled = true).start() + fakeExecutor.runAllReady() verify(controlsController, never()).bindComponentForPanel(any()) } @@ -221,6 +233,12 @@ class ControlsStartableTest : SysuiTestCase() { verify(controlsController, never()).setPreferredSelection(any()) } + private fun setUpControlsListingControls(listings: List<ControlsServiceInfo>) { + doAnswer { doReturn(listings).`when`(controlsListingController).getCurrentServices() } + .`when`(controlsListingController) + .forceReload() + } + private fun createStartable(enabled: Boolean): ControlsStartable { val component: ControlsComponent = mock() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java index 068852de7a43..95c689737417 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java @@ -92,9 +92,6 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<Observer<Collection<ComplicationViewModel>>> mObserverCaptor; - @Captor - private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor; - @Complication.Category static final int COMPLICATION_CATEGORY = Complication.CATEGORY_SYSTEM; @@ -189,8 +186,6 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { // Dream entry animations finished. when(mDreamOverlayStateController.areEntryAnimationsFinished()).thenReturn(true); - final DreamOverlayStateController.Callback stateCallback = captureOverlayStateCallback(); - stateCallback.onStateChanged(); // Add a complication after entry animations are finished. final HashSet<ComplicationViewModel> complications = new HashSet<>( @@ -223,9 +218,4 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { mObserverCaptor.capture()); return mObserverCaptor.getValue(); } - - private DreamOverlayStateController.Callback captureOverlayStateCallback() { - verify(mDreamOverlayStateController).addCallback(mCallbackCaptor.capture()); - return mCallbackCaptor.getValue(); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/ConditionalRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/ConditionalRestarterTest.kt new file mode 100644 index 000000000000..0e14591c5f53 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/ConditionalRestarterTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.flags + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! + */ +@SmallTest +class ConditionalRestarterTest : SysuiTestCase() { + private lateinit var restarter: ConditionalRestarter + + @Mock private lateinit var systemExitRestarter: SystemExitRestarter + + val restartDelayMs = 0L + val dispatcher = StandardTestDispatcher() + val testScope = TestScope(dispatcher) + + val conditionA = FakeCondition() + val conditionB = FakeCondition() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + restarter = + ConditionalRestarter( + systemExitRestarter, + setOf(conditionA, conditionB), + restartDelayMs, + testScope, + dispatcher + ) + } + + @Test + fun restart_ImmediatelySatisfied() = + testScope.runTest { + conditionA.canRestart = true + conditionB.canRestart = true + restarter.restartSystemUI("Restart for test") + advanceUntilIdle() + verify(systemExitRestarter).restartSystemUI(any()) + } + + @Test + fun restart_WaitsForConditionA() = + testScope.runTest { + conditionA.canRestart = false + conditionB.canRestart = true + + restarter.restartSystemUI("Restart for test") + advanceUntilIdle() + // No restart occurs yet. + verify(systemExitRestarter, never()).restartSystemUI(any()) + + conditionA.canRestart = true + conditionA.retryFn?.invoke() + advanceUntilIdle() + verify(systemExitRestarter).restartSystemUI(any()) + } + + @Test + fun restart_WaitsForConditionB() = + testScope.runTest { + conditionA.canRestart = true + conditionB.canRestart = false + + restarter.restartSystemUI("Restart for test") + advanceUntilIdle() + // No restart occurs yet. + verify(systemExitRestarter, never()).restartSystemUI(any()) + + conditionB.canRestart = true + conditionB.retryFn?.invoke() + advanceUntilIdle() + verify(systemExitRestarter).restartSystemUI(any()) + } + + @Test + fun restart_WaitsForAllConditions() = + testScope.runTest { + conditionA.canRestart = true + conditionB.canRestart = false + + restarter.restartSystemUI("Restart for test") + advanceUntilIdle() + // No restart occurs yet. + verify(systemExitRestarter, never()).restartSystemUI(any()) + + // B becomes true, but A is now false + conditionA.canRestart = false + conditionB.canRestart = true + conditionB.retryFn?.invoke() + advanceUntilIdle() + // No restart occurs yet. + verify(systemExitRestarter, never()).restartSystemUI(any()) + + conditionA.canRestart = true + conditionA.retryFn?.invoke() + advanceUntilIdle() + verify(systemExitRestarter).restartSystemUI(any()) + } + + class FakeCondition : ConditionalRestarter.Condition { + var retryFn: (() -> Unit)? = null + var canRestart = false + + override fun canRestartNow(retryFn: () -> Unit): Boolean { + this.retryFn = retryFn + + return canRestart + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt deleted file mode 100644 index 6060afe495f5..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseRestarterTest.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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.flags - -import android.test.suitebuilder.annotation.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.WakefulnessLifecycle -import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP -import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE -import com.android.systemui.statusbar.policy.BatteryController -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test -import org.mockito.ArgumentCaptor -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever -import org.mockito.MockitoAnnotations - -/** - * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! - */ -@SmallTest -class FeatureFlagsReleaseRestarterTest : SysuiTestCase() { - private lateinit var restarter: FeatureFlagsReleaseRestarter - - @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock private lateinit var batteryController: BatteryController - @Mock private lateinit var systemExitRestarter: SystemExitRestarter - private val executor = FakeExecutor(FakeSystemClock()) - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - restarter = - FeatureFlagsReleaseRestarter( - wakefulnessLifecycle, - batteryController, - executor, - systemExitRestarter - ) - } - - @Test - fun testRestart_ScheduledWhenReady() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(true) - - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - assertThat(executor.numPending()).isEqualTo(1) - } - - @Test - fun testRestart_RestartsWhenIdle() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(true) - - restarter.restartSystemUI("Restart for test") - verify(systemExitRestarter, never()).restartSystemUI("Restart for test") - executor.advanceClockToLast() - executor.runAllReady() - verify(systemExitRestarter).restartSystemUI(any()) - } - - @Test - fun testRestart_NotScheduledWhenAwake() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) - whenever(batteryController.isPluggedIn).thenReturn(true) - - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - assertThat(executor.numPending()).isEqualTo(0) - } - - @Test - fun testRestart_NotScheduledWhenNotPluggedIn() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(false) - - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - assertThat(executor.numPending()).isEqualTo(0) - } - - @Test - fun testRestart_NotDoubleSheduled() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(true) - - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - restarter.restartSystemUI("Restart for test") - assertThat(executor.numPending()).isEqualTo(1) - } - - @Test - fun testWakefulnessLifecycle_CanRestart() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) - whenever(batteryController.isPluggedIn).thenReturn(true) - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - - val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java) - verify(wakefulnessLifecycle).addObserver(captor.capture()) - - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - - captor.value.onFinishedGoingToSleep() - assertThat(executor.numPending()).isEqualTo(1) - } - - @Test - fun testBatteryController_CanRestart() { - whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - whenever(batteryController.isPluggedIn).thenReturn(false) - assertThat(executor.numPending()).isEqualTo(0) - restarter.restartSystemUI("Restart for test") - - val captor = - ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java) - verify(batteryController).addCallback(captor.capture()) - - whenever(batteryController.isPluggedIn).thenReturn(true) - - captor.value.onBatteryLevelChanged(0, true, true) - assertThat(executor.numPending()).isEqualTo(1) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/PluggedInConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/PluggedInConditionTest.kt new file mode 100644 index 000000000000..647b05a77b90 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/PluggedInConditionTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.flags + +import android.test.suitebuilder.annotation.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.BatteryController +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +/** + * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! + */ +@SmallTest +class PluggedInConditionTest : SysuiTestCase() { + private lateinit var condition: PluggedInCondition + + @Mock private lateinit var batteryController: BatteryController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + condition = PluggedInCondition(batteryController) + } + + @Test + fun testCondition_unplugged() { + whenever(batteryController.isPluggedIn).thenReturn(false) + + assertThat(condition.canRestartNow({})).isFalse() + } + + @Test + fun testCondition_pluggedIn() { + whenever(batteryController.isPluggedIn).thenReturn(true) + + assertThat(condition.canRestartNow({})).isTrue() + } + + @Test + fun testCondition_invokesRetry() { + whenever(batteryController.isPluggedIn).thenReturn(false) + var retried = false + val retryFn = { retried = true } + + // No restart yet, but we do register a listener now. + assertThat(condition.canRestartNow(retryFn)).isFalse() + val captor = + ArgumentCaptor.forClass(BatteryController.BatteryStateChangeCallback::class.java) + verify(batteryController).addCallback(captor.capture()) + + whenever(batteryController.isPluggedIn).thenReturn(true) + + captor.value.onBatteryLevelChanged(0, true, true) + assertThat(retried).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/ScreenIdleConditionTest.kt index 686782f59355..f7a773ea30ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugRestarterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/ScreenIdleConditionTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,11 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE -import com.android.systemui.util.mockito.any +import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.Mock -import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations @@ -34,37 +33,45 @@ import org.mockito.MockitoAnnotations * Be careful with the {FeatureFlagsReleaseRestarter} in this test. It has a call to System.exit()! */ @SmallTest -class FeatureFlagsDebugRestarterTest : SysuiTestCase() { - private lateinit var restarter: FeatureFlagsDebugRestarter +class ScreenIdleConditionTest : SysuiTestCase() { + private lateinit var condition: ScreenIdleCondition @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle - @Mock private lateinit var systemExitRestarter: SystemExitRestarter @Before fun setup() { MockitoAnnotations.initMocks(this) - restarter = FeatureFlagsDebugRestarter(wakefulnessLifecycle, systemExitRestarter) + condition = ScreenIdleCondition(wakefulnessLifecycle) } @Test - fun testRestart_ImmediateWhenAsleep() { + fun testCondition_awake() { + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) + + assertThat(condition.canRestartNow {}).isFalse() + } + + @Test + fun testCondition_asleep() { whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - restarter.restartSystemUI("Restart for test") - verify(systemExitRestarter).restartSystemUI(any()) + + assertThat(condition.canRestartNow {}).isTrue() } @Test - fun testRestart_WaitsForSceenOff() { + fun testCondition_invokesRetry() { whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_AWAKE) + var retried = false + val retryFn = { retried = true } - restarter.restartSystemUI("Restart for test") - verify(systemExitRestarter, never()).restartSystemUI(any()) - + // No restart yet, but we do register a listener now. + assertThat(condition.canRestartNow(retryFn)).isFalse() val captor = ArgumentCaptor.forClass(WakefulnessLifecycle.Observer::class.java) verify(wakefulnessLifecycle).addObserver(captor.capture()) - captor.value.onFinishedGoingToSleep() + whenever(wakefulnessLifecycle.wakefulness).thenReturn(WAKEFULNESS_ASLEEP) - verify(systemExitRestarter).restartSystemUI(any()) + captor.value.onFinishedGoingToSleep() + assertThat(retried).isTrue() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 62c9e5ffbb51..5528b94a691c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -281,6 +281,24 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { } @Test + fun `quickAffordance - hidden when quick settings is visible`() = + testScope.runTest { + repository.setQuickSettingsVisible(true) + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = ICON, + ) + ) + + val collectedValue = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + ) + + assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) + } + + @Test fun `quickAffordance - bottom start affordance hidden while dozing`() = testScope.runTest { repository.setDozing(true) 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 fe9098fa5c25..5cd24e675a54 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 @@ -358,6 +358,50 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun `LOCKSCREEN to DREAMING`() = + testScope.runTest { + // GIVEN a device that is not dreaming or dozing + keyguardRepository.setDreamingWithOverlay(false) + keyguardRepository.setWakefulnessModel(startingToWake()) + keyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + reset(mockTransitionRepository) + + // WHEN the device begins to dream + keyguardRepository.setDreamingWithOverlay(true) + advanceUntilIdle() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) + } + // THEN a transition to DREAMING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.DREAMING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun `LOCKSCREEN to DOZING`() = testScope.runTest { // GIVEN a device with AOD not available @@ -967,6 +1011,92 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun `OCCLUDED to GONE`() = + testScope.runTest { + // GIVEN a device on lockscreen + keyguardRepository.setKeyguardShowing(true) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + keyguardRepository.setKeyguardOccluded(true) + runCurrent() + reset(mockTransitionRepository) + + // WHEN keyguard goes away + keyguardRepository.setKeyguardShowing(false) + // AND occlusion ends + keyguardRepository.setKeyguardOccluded(false) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) + } + // THEN a transition to GONE should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.GONE) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `OCCLUDED to LOCKSCREEN`() = + testScope.runTest { + // GIVEN a device on lockscreen + keyguardRepository.setKeyguardShowing(true) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + keyguardRepository.setKeyguardOccluded(true) + runCurrent() + reset(mockTransitionRepository) + + // WHEN occlusion ends + keyguardRepository.setKeyguardOccluded(false) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture(), anyBoolean()) + } + // THEN a transition to LOCKSCREEN should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + private fun startingToWake() = WakefulnessModel( WakefulnessState.STARTING_TO_WAKE, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index 2a91799741b7..746f66881a88 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -21,7 +21,9 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.ScrimAlpha import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -44,21 +46,86 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { private lateinit var underTest: PrimaryBouncerToGoneTransitionViewModel private lateinit var repository: FakeKeyguardTransitionRepository @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController + @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor @Before fun setUp() { MockitoAnnotations.initMocks(this) repository = FakeKeyguardTransitionRepository() val interactor = KeyguardTransitionInteractor(repository) - underTest = PrimaryBouncerToGoneTransitionViewModel(interactor, statusBarStateController) + underTest = + PrimaryBouncerToGoneTransitionViewModel( + interactor, + statusBarStateController, + primaryBouncerInteractor + ) + + whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(false) + whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false) } @Test - fun scrimBehindAlpha_leaveShadeOpen() = + fun bouncerAlpha() = runTest(UnconfinedTestDispatcher()) { val values = mutableListOf<Float>() - val job = underTest.scrimBehindAlpha.onEach { values.add(it) }.launchIn(this) + val job = underTest.bouncerAlpha.onEach { values.add(it) }.launchIn(this) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.6f)) + + assertThat(values.size).isEqualTo(3) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } + + job.cancel() + } + + @Test + fun bouncerAlpha_runDimissFromKeyguard() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.bouncerAlpha.onEach { values.add(it) }.launchIn(this) + + whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.6f)) + + assertThat(values.size).isEqualTo(3) + values.forEach { assertThat(it).isEqualTo(0f) } + + job.cancel() + } + + @Test + fun scrimAlpha_runDimissFromKeyguard() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<ScrimAlpha>() + + val job = underTest.scrimAlpha.onEach { values.add(it) }.launchIn(this) + + whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true) + + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.6f)) + repository.sendTransitionStep(step(1f)) + + assertThat(values.size).isEqualTo(4) + values.forEach { assertThat(it).isEqualTo(ScrimAlpha(notificationsAlpha = 1f)) } + + job.cancel() + } + + @Test + fun scrimBehindAlpha_leaveShadeOpen() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<ScrimAlpha>() + + val job = underTest.scrimAlpha.onEach { values.add(it) }.launchIn(this) whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(true) @@ -68,7 +135,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { repository.sendTransitionStep(step(1f)) assertThat(values.size).isEqualTo(4) - values.forEach { assertThat(it).isEqualTo(1f) } + values.forEach { + assertThat(it).isEqualTo(ScrimAlpha(notificationsAlpha = 1f, behindAlpha = 1f)) + } job.cancel() } @@ -76,9 +145,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { @Test fun scrimBehindAlpha_doNotLeaveShadeOpen() = runTest(UnconfinedTestDispatcher()) { - val values = mutableListOf<Float>() + val values = mutableListOf<ScrimAlpha>() - val job = underTest.scrimBehindAlpha.onEach { values.add(it) }.launchIn(this) + val job = underTest.scrimAlpha.onEach { values.add(it) }.launchIn(this) whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false) @@ -88,8 +157,10 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { repository.sendTransitionStep(step(1f)) assertThat(values.size).isEqualTo(4) - values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } - assertThat(values[3]).isEqualTo(0f) + values.forEach { assertThat(it.notificationsAlpha).isEqualTo(0f) } + values.forEach { assertThat(it.frontAlpha).isEqualTo(0f) } + values.forEach { assertThat(it.behindAlpha).isIn(Range.closed(0f, 1f)) } + assertThat(values[3].behindAlpha).isEqualTo(0f) job.cancel() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt index c7f3fa0830cd..fb20bacee64c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt @@ -121,4 +121,92 @@ class TableChangeTest : SysuiTestCase() { assertThat(underTest.getName()).doesNotContain("original") assertThat(underTest.getVal()).isEqualTo("8900") } + + @Test + fun updateTo_emptyToString_isString() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + + val new = TableChange(columnPrefix = "newPrefix", columnName = "newName") + new.set("newString") + underTest.updateTo(new) + + assertThat(underTest.hasData()).isTrue() + assertThat(underTest.getName()).contains("newPrefix") + assertThat(underTest.getName()).contains("newName") + assertThat(underTest.getVal()).isEqualTo("newString") + } + + @Test + fun updateTo_intToEmpty_isEmpty() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + underTest.set(42) + + val new = TableChange(columnPrefix = "newPrefix", columnName = "newName") + underTest.updateTo(new) + + assertThat(underTest.hasData()).isFalse() + assertThat(underTest.getName()).contains("newPrefix") + assertThat(underTest.getName()).contains("newName") + assertThat(underTest.getVal()).isEqualTo("null") + } + + @Test + fun updateTo_stringToBool_isBool() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + underTest.set("oldString") + + val new = TableChange(columnPrefix = "newPrefix", columnName = "newName") + new.set(true) + underTest.updateTo(new) + + assertThat(underTest.hasData()).isTrue() + assertThat(underTest.getName()).contains("newPrefix") + assertThat(underTest.getName()).contains("newName") + assertThat(underTest.getVal()).isEqualTo("true") + } + + @Test + fun updateTo_intToString_isString() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + underTest.set(43) + + val new = TableChange(columnPrefix = "newPrefix", columnName = "newName") + new.set("newString") + underTest.updateTo(new) + + assertThat(underTest.hasData()).isTrue() + assertThat(underTest.getName()).contains("newPrefix") + assertThat(underTest.getName()).contains("newName") + assertThat(underTest.getVal()).isEqualTo("newString") + } + + @Test + fun updateTo_boolToInt_isInt() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + underTest.set(false) + + val new = TableChange(columnPrefix = "newPrefix", columnName = "newName") + new.set(44) + underTest.updateTo(new) + + assertThat(underTest.hasData()).isTrue() + assertThat(underTest.getName()).contains("newPrefix") + assertThat(underTest.getName()).contains("newName") + assertThat(underTest.getVal()).isEqualTo("44") + } + + @Test + fun updateTo_boolToNewBool_isNewBool() { + val underTest = TableChange(columnPrefix = "fakePrefix", columnName = "fakeName") + underTest.set(false) + + val new = TableChange(columnPrefix = "newPrefix", columnName = "newName") + new.set(true) + underTest.updateTo(new) + + assertThat(underTest.hasData()).isTrue() + assertThat(underTest.getName()).contains("newPrefix") + assertThat(underTest.getName()).contains("newName") + assertThat(underTest.getVal()).isEqualTo("true") + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt index 2c8d7abd4f4a..949fa1cce0cb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt @@ -435,11 +435,236 @@ class TableLogBufferTest : SysuiTestCase() { assertThat(dumpedString).doesNotContain("testString[0]") assertThat(dumpedString).doesNotContain("testString[1]") - assertThat(dumpedString).doesNotContain("testString[2]") + // The buffer should contain [MAX_SIZE + 1] entries since we also save the most recently + // evicted value. + assertThat(dumpedString).contains("testString[2]") assertThat(dumpedString).contains("testString[3]") assertThat(dumpedString).contains("testString[${MAX_SIZE + 2}]") } + @Test + fun columnEvicted_lastKnownColumnValueInDump() { + systemClock.setCurrentTimeMillis(100L) + underTest.logChange(prefix = "", columnName = "willBeEvicted", value = "evictedValue") + + // Exactly fill the buffer so that "willBeEvicted" is evicted + for (i in 0 until MAX_SIZE) { + systemClock.advanceTime(100L) + val dumpString = "fillString[$i]" + underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString) + } + + val dumpedString = dumpChanges() + + // Expect that we'll have both the evicted column entry... + val evictedColumnLog = + TABLE_LOG_DATE_FORMAT.format(100L) + + SEPARATOR + + "willBeEvicted" + + SEPARATOR + + "evictedValue" + assertThat(dumpedString).contains(evictedColumnLog) + + // ... *and* all of the fillingColumn entries. + val firstFillingColumnLog = + TABLE_LOG_DATE_FORMAT.format(200L) + + SEPARATOR + + "fillingColumn" + + SEPARATOR + + "fillString[0]" + val lastFillingColumnLog = + TABLE_LOG_DATE_FORMAT.format(1100L) + + SEPARATOR + + "fillingColumn" + + SEPARATOR + + "fillString[9]" + assertThat(dumpedString).contains(firstFillingColumnLog) + assertThat(dumpedString).contains(lastFillingColumnLog) + } + + @Test + fun multipleColumnsEvicted_allColumnsInDump() { + systemClock.setCurrentTimeMillis(100L) + underTest.logChange(prefix = "", columnName = "willBeEvictedString", value = "evictedValue") + systemClock.advanceTime(100L) + underTest.logChange(prefix = "", columnName = "willBeEvictedInt", value = 45) + systemClock.advanceTime(100L) + underTest.logChange(prefix = "", columnName = "willBeEvictedBool", value = true) + + // Exactly fill the buffer so that all the above columns will be evicted + for (i in 0 until MAX_SIZE) { + systemClock.advanceTime(100L) + val dumpString = "fillString[$i]" + underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString) + } + + val dumpedString = dumpChanges() + + // Expect that we'll have all the evicted column entries... + val evictedColumnLogString = + TABLE_LOG_DATE_FORMAT.format(100L) + + SEPARATOR + + "willBeEvictedString" + + SEPARATOR + + "evictedValue" + val evictedColumnLogInt = + TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + "willBeEvictedInt" + SEPARATOR + "45" + val evictedColumnLogBool = + TABLE_LOG_DATE_FORMAT.format(300L) + + SEPARATOR + + "willBeEvictedBool" + + SEPARATOR + + "true" + assertThat(dumpedString).contains(evictedColumnLogString) + assertThat(dumpedString).contains(evictedColumnLogInt) + assertThat(dumpedString).contains(evictedColumnLogBool) + + // ... *and* all of the fillingColumn entries. + val firstFillingColumnLog = + TABLE_LOG_DATE_FORMAT.format(400) + + SEPARATOR + + "fillingColumn" + + SEPARATOR + + "fillString[0]" + val lastFillingColumnLog = + TABLE_LOG_DATE_FORMAT.format(1300) + + SEPARATOR + + "fillingColumn" + + SEPARATOR + + "fillString[9]" + assertThat(dumpedString).contains(firstFillingColumnLog) + assertThat(dumpedString).contains(lastFillingColumnLog) + } + + @Test + fun multipleColumnsEvicted_differentPrefixSameName_allColumnsInDump() { + systemClock.setCurrentTimeMillis(100L) + underTest.logChange(prefix = "prefix1", columnName = "sameName", value = "value1") + systemClock.advanceTime(100L) + underTest.logChange(prefix = "prefix2", columnName = "sameName", value = "value2") + systemClock.advanceTime(100L) + underTest.logChange(prefix = "prefix3", columnName = "sameName", value = "value3") + + // Exactly fill the buffer so that all the above columns will be evicted + for (i in 0 until MAX_SIZE) { + systemClock.advanceTime(100L) + val dumpString = "fillString[$i]" + underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString) + } + + val dumpedString = dumpChanges() + + // Expect that we'll have all the evicted column entries + val evictedColumn1 = + TABLE_LOG_DATE_FORMAT.format(100L) + + SEPARATOR + + "prefix1.sameName" + + SEPARATOR + + "value1" + val evictedColumn2 = + TABLE_LOG_DATE_FORMAT.format(200L) + + SEPARATOR + + "prefix2.sameName" + + SEPARATOR + + "value2" + val evictedColumn3 = + TABLE_LOG_DATE_FORMAT.format(300L) + + SEPARATOR + + "prefix3.sameName" + + SEPARATOR + + "value3" + assertThat(dumpedString).contains(evictedColumn1) + assertThat(dumpedString).contains(evictedColumn2) + assertThat(dumpedString).contains(evictedColumn3) + } + + @Test + fun multipleColumnsEvicted_dumpSortedByTimestamp() { + systemClock.setCurrentTimeMillis(100L) + underTest.logChange(prefix = "", columnName = "willBeEvictedFirst", value = "evictedValue") + systemClock.advanceTime(100L) + underTest.logChange(prefix = "", columnName = "willBeEvictedSecond", value = 45) + systemClock.advanceTime(100L) + underTest.logChange(prefix = "", columnName = "willBeEvictedThird", value = true) + + // Exactly fill the buffer with so that all the above columns will be evicted + for (i in 0 until MAX_SIZE) { + systemClock.advanceTime(100L) + val dumpString = "fillString[$i]" + underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString) + } + + val dumpedString = dumpChanges() + + // Expect that we'll have all the evicted column entries in timestamp order + val firstEvictedLog = + TABLE_LOG_DATE_FORMAT.format(100L) + + SEPARATOR + + "willBeEvictedFirst" + + SEPARATOR + + "evictedValue" + val secondEvictedLog = + TABLE_LOG_DATE_FORMAT.format(200L) + + SEPARATOR + + "willBeEvictedSecond" + + SEPARATOR + + "45" + val thirdEvictedLog = + TABLE_LOG_DATE_FORMAT.format(300L) + + SEPARATOR + + "willBeEvictedThird" + + SEPARATOR + + "true" + assertThat(dumpedString).contains(firstEvictedLog) + val stringAfterFirst = dumpedString.substringAfter(firstEvictedLog) + assertThat(stringAfterFirst).contains(secondEvictedLog) + val stringAfterSecond = stringAfterFirst.substringAfter(secondEvictedLog) + assertThat(stringAfterSecond).contains(thirdEvictedLog) + } + + @Test + fun sameColumnEvictedMultipleTimes_onlyLastEvictionInDump() { + systemClock.setCurrentTimeMillis(0L) + + for (i in 1 until 4) { + systemClock.advanceTime(100L) + val dumpString = "evicted[$i]" + underTest.logChange(prefix = "", columnName = "evictedColumn", value = dumpString) + } + + // Exactly fill the buffer so that all the entries for "evictedColumn" will be evicted. + for (i in 0 until MAX_SIZE) { + systemClock.advanceTime(100L) + val dumpString = "fillString[$i]" + underTest.logChange(prefix = "", columnName = "fillingColumn", value = dumpString) + } + + val dumpedString = dumpChanges() + + // Expect that we only have the most recent evicted column entry + val evictedColumnLog1 = + TABLE_LOG_DATE_FORMAT.format(100L) + + SEPARATOR + + "evictedColumn" + + SEPARATOR + + "evicted[1]" + val evictedColumnLog2 = + TABLE_LOG_DATE_FORMAT.format(200L) + + SEPARATOR + + "evictedColumn" + + SEPARATOR + + "evicted[2]" + val evictedColumnLog3 = + TABLE_LOG_DATE_FORMAT.format(300L) + + SEPARATOR + + "evictedColumn" + + SEPARATOR + + "evicted[3]" + assertThat(dumpedString).doesNotContain(evictedColumnLog1) + assertThat(dumpedString).doesNotContain(evictedColumnLog2) + assertThat(dumpedString).contains(evictedColumnLog3) + } + private fun dumpChanges(): String { underTest.dump(PrintWriter(outputWriter), arrayOf()) return outputWriter.toString() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index ab0669a28f04..d428db7b9dda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -2031,7 +2031,7 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test - fun testRetain_sessionPlayer_destroyedWhileActive_fullyRemoved() { + fun testRetain_sessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) addPlaybackStateAction() @@ -2051,6 +2051,40 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testRetain_sessionPlayer_canResume_destroyedWhileActive_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions and that does allow resumption is added, + addNotificationAndLoad() + val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {}) + mediaDataManager.onMediaDataLoaded(KEY, null, dataResumable) + + // And then the session is destroyed without timing out first + sessionCallbackCaptor.value.invoke(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test fun testSessionDestroyed_noNotificationKey_stillRemoved() { whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt index 4dfa6261b868..9ab728949e40 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt @@ -313,6 +313,25 @@ class MediaResumeListenerTest : SysuiTestCase() { } @Test + fun testOnLoadTwice_onlyChecksOnce() { + // When data is first loaded, + setUpMbsWithValidResolveInfo() + resumeListener.onMediaDataLoaded(KEY, null, data) + + // We notify the manager to set a null action + verify(mediaDataManager).setResumeAction(KEY, null) + + // If we then get another update from the app before the first check completes + assertThat(executor.numPending()).isEqualTo(1) + var dataWithCheck = data.copy(hasCheckedForResume = true) + resumeListener.onMediaDataLoaded(KEY, null, dataWithCheck) + + // We do not try to start another check + assertThat(executor.numPending()).isEqualTo(1) + verify(mediaDataManager).setResumeAction(KEY, null) + } + + @Test fun testOnUserUnlock_loadsTracks() { // Set up mock service to successfully find valid media val description = MediaDescription.Builder().setTitle(TITLE).build() @@ -392,7 +411,7 @@ class MediaResumeListenerTest : SysuiTestCase() { assertThat(result.size).isEqualTo(3) assertThat(result[2].toLong()).isEqualTo(currentTime) } - verify(sharedPrefsEditor, times(1)).apply() + verify(sharedPrefsEditor).apply() } @Test @@ -432,8 +451,8 @@ class MediaResumeListenerTest : SysuiTestCase() { resumeListener.userUnlockReceiver.onReceive(mockContext, intent) // We add its resume controls - verify(resumeBrowser, times(1)).findRecentMedia() - verify(mediaDataManager, times(1)) + verify(resumeBrowser).findRecentMedia() + verify(mediaDataManager) .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) } @@ -516,7 +535,7 @@ class MediaResumeListenerTest : SysuiTestCase() { assertThat(result.size).isEqualTo(3) assertThat(result[2].toLong()).isEqualTo(currentTime) } - verify(sharedPrefsEditor, times(1)).apply() + verify(sharedPrefsEditor).apply() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index 89606bf6be3d..0ab0e2b4b9f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -52,6 +52,8 @@ import com.android.systemui.R; import com.android.systemui.SysuiBaseFragmentTest; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.media.controls.ui.MediaHost; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSFragmentComponent; @@ -60,6 +62,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.settings.FakeDisplayTracker; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; @@ -103,6 +106,8 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { @Mock private QSSquishinessController mSquishinessController; @Mock private FooterActionsViewModel mFooterActionsViewModel; @Mock private FooterActionsViewModel.Factory mFooterActionsViewModelFactory; + @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; + @Mock private FeatureFlags mFeatureFlags; private View mQsFragmentView; public QSFragmentTest() { @@ -148,8 +153,9 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { } @Test - public void transitionToFullShade_setsAlphaUsingShadeInterpolator() { + public void transitionToFullShade_smallScreen_alphaAlways1() { QSFragment fragment = resumeAndGetFragment(); + setIsSmallScreen(); setStatusBarCurrentAndUpcomingState(StatusBarState.SHADE); boolean isTransitioningToFullShade = true; float transitionProgress = 0.5f; @@ -158,6 +164,43 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress, squishinessFraction); + assertThat(mQsFragmentView.getAlpha()).isEqualTo(1f); + } + + @Test + public void transitionToFullShade_largeScreen_flagEnabled_alphaLargeScreenShadeInterpolator() { + when(mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) + .thenReturn(true); + QSFragment fragment = resumeAndGetFragment(); + setIsLargeScreen(); + setStatusBarCurrentAndUpcomingState(StatusBarState.SHADE); + boolean isTransitioningToFullShade = true; + float transitionProgress = 0.5f; + float squishinessFraction = 0.5f; + when(mLargeScreenShadeInterpolator.getQsAlpha(transitionProgress)).thenReturn(123f); + + fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress, + squishinessFraction); + + assertThat(mQsFragmentView.getAlpha()) + .isEqualTo(123f); + } + + @Test + public void transitionToFullShade_largeScreen_flagDisabled_alphaStandardInterpolator() { + when(mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) + .thenReturn(false); + QSFragment fragment = resumeAndGetFragment(); + setIsLargeScreen(); + setStatusBarCurrentAndUpcomingState(StatusBarState.SHADE); + boolean isTransitioningToFullShade = true; + float transitionProgress = 0.5f; + float squishinessFraction = 0.5f; + when(mLargeScreenShadeInterpolator.getQsAlpha(transitionProgress)).thenReturn(123f); + + fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress, + squishinessFraction); + assertThat(mQsFragmentView.getAlpha()) .isEqualTo(ShadeInterpolation.getContentAlpha(transitionProgress)); } @@ -514,7 +557,9 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { mock(DumpManager.class), mock(QSLogger.class), mock(FooterActionsController.class), - mFooterActionsViewModelFactory); + mFooterActionsViewModelFactory, + mLargeScreenShadeInterpolator, + mFeatureFlags); } private void setUpOther() { @@ -622,4 +667,12 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { return null; }).when(view).getLocationOnScreen(any(int[].class)); } + + private void setIsLargeScreen() { + getFragment().setIsNotificationPanelFullWidth(false); + } + + private void setIsSmallScreen() { + getFragment().setIsNotificationPanelFullWidth(true); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 4f469f753bdf..2dfb6e564d99 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -22,8 +22,6 @@ import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.google.common.truth.Truth.assertThat; -import static kotlinx.coroutines.flow.FlowKt.emptyFlow; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -35,6 +33,8 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.flow.FlowKt.emptyFlow; + import android.annotation.IdRes; import android.content.ContentResolver; import android.content.res.Configuration; diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolatorImplTest.kt new file mode 100644 index 000000000000..8309342d2c60 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/LargeScreenShadeInterpolatorImplTest.kt @@ -0,0 +1,144 @@ +package com.android.systemui.shade.transition + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.FakeConfigurationController +import com.google.common.truth.Expect +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class LargeScreenShadeInterpolatorImplTest : SysuiTestCase() { + @get:Rule val expect: Expect = Expect.create() + + private val portraitShadeInterpolator = LargeScreenPortraitShadeInterpolator() + private val splitShadeInterpolator = SplitShadeInterpolator() + private val configurationController = FakeConfigurationController() + private val impl = + LargeScreenShadeInterpolatorImpl( + configurationController, + context, + splitShadeInterpolator, + portraitShadeInterpolator + ) + + @Test + fun getBehindScrimAlpha_inSplitShade_usesSplitShadeValue() { + setSplitShadeEnabled(true) + + assertInterpolation( + actual = { fraction -> impl.getBehindScrimAlpha(fraction) }, + expected = { fraction -> splitShadeInterpolator.getBehindScrimAlpha(fraction) } + ) + } + + @Test + fun getBehindScrimAlpha_inPortraitShade_usesPortraitShadeValue() { + setSplitShadeEnabled(false) + + assertInterpolation( + actual = { fraction -> impl.getBehindScrimAlpha(fraction) }, + expected = { fraction -> portraitShadeInterpolator.getBehindScrimAlpha(fraction) } + ) + } + + @Test + fun getNotificationScrimAlpha_inSplitShade_usesSplitShadeValue() { + setSplitShadeEnabled(true) + + assertInterpolation( + actual = { fraction -> impl.getNotificationScrimAlpha(fraction) }, + expected = { fraction -> splitShadeInterpolator.getNotificationScrimAlpha(fraction) } + ) + } + @Test + fun getNotificationScrimAlpha_inPortraitShade_usesPortraitShadeValue() { + setSplitShadeEnabled(false) + + assertInterpolation( + actual = { fraction -> impl.getNotificationScrimAlpha(fraction) }, + expected = { fraction -> portraitShadeInterpolator.getNotificationScrimAlpha(fraction) } + ) + } + + @Test + fun getNotificationContentAlpha_inSplitShade_usesSplitShadeValue() { + setSplitShadeEnabled(true) + + assertInterpolation( + actual = { fraction -> impl.getNotificationContentAlpha(fraction) }, + expected = { fraction -> splitShadeInterpolator.getNotificationContentAlpha(fraction) } + ) + } + + @Test + fun getNotificationContentAlpha_inPortraitShade_usesPortraitShadeValue() { + setSplitShadeEnabled(false) + + assertInterpolation( + actual = { fraction -> impl.getNotificationContentAlpha(fraction) }, + expected = { fraction -> + portraitShadeInterpolator.getNotificationContentAlpha(fraction) + } + ) + } + + @Test + fun getNotificationFooterAlpha_inSplitShade_usesSplitShadeValue() { + setSplitShadeEnabled(true) + + assertInterpolation( + actual = { fraction -> impl.getNotificationFooterAlpha(fraction) }, + expected = { fraction -> splitShadeInterpolator.getNotificationFooterAlpha(fraction) } + ) + } + @Test + fun getNotificationFooterAlpha_inPortraitShade_usesPortraitShadeValue() { + setSplitShadeEnabled(false) + + assertInterpolation( + actual = { fraction -> impl.getNotificationFooterAlpha(fraction) }, + expected = { fraction -> + portraitShadeInterpolator.getNotificationFooterAlpha(fraction) + } + ) + } + + @Test + fun getQsAlpha_inSplitShade_usesSplitShadeValue() { + setSplitShadeEnabled(true) + + assertInterpolation( + actual = { fraction -> impl.getQsAlpha(fraction) }, + expected = { fraction -> splitShadeInterpolator.getQsAlpha(fraction) } + ) + } + @Test + fun getQsAlpha_inPortraitShade_usesPortraitShadeValue() { + setSplitShadeEnabled(false) + + assertInterpolation( + actual = { fraction -> impl.getQsAlpha(fraction) }, + expected = { fraction -> portraitShadeInterpolator.getQsAlpha(fraction) } + ) + } + + private fun setSplitShadeEnabled(enabled: Boolean) { + overrideResource(R.bool.config_use_split_notification_shade, enabled) + configurationController.notifyConfigurationChanged() + } + + private fun assertInterpolation( + actual: (fraction: Float) -> Float, + expected: (fraction: Float) -> Float + ) { + for (i in 0..10) { + val fraction = i / 10f + expect.that(actual(fraction)).isEqualTo(expected(fraction)) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/LinearLargeScreenShadeInterpolator.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/LinearLargeScreenShadeInterpolator.kt new file mode 100644 index 000000000000..d24bcdc834a7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/LinearLargeScreenShadeInterpolator.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.transition + +class LinearLargeScreenShadeInterpolator : LargeScreenShadeInterpolator { + override fun getBehindScrimAlpha(fraction: Float) = fraction + override fun getNotificationScrimAlpha(fraction: Float) = fraction + override fun getNotificationContentAlpha(fraction: Float) = fraction + override fun getNotificationFooterAlpha(fraction: Float) = fraction + override fun getQsAlpha(fraction: Float) = fraction +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt index 84f86561d073..cbf54854759b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt @@ -5,6 +5,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.shade.STATE_CLOSED import com.android.systemui.shade.STATE_OPEN import com.android.systemui.shade.STATE_OPENING @@ -30,6 +32,7 @@ class ScrimShadeTransitionControllerTest : SysuiTestCase() { @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController @Mock private lateinit var headsUpManager: HeadsUpManager + @Mock private lateinit var featureFlags: FeatureFlags private val configurationController = FakeConfigurationController() private lateinit var controller: ScrimShadeTransitionController @@ -45,7 +48,8 @@ class ScrimShadeTransitionControllerTest : SysuiTestCase() { scrimController, context.resources, statusBarStateController, - headsUpManager) + headsUpManager, + featureFlags) controller.onPanelStateChanged(STATE_OPENING) } @@ -107,6 +111,19 @@ class ScrimShadeTransitionControllerTest : SysuiTestCase() { } @Test + fun onPanelExpansionChanged_inSplitShade_flagTrue_setsFractionEqualToEventFraction() { + whenever(featureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) + .thenReturn(true) + whenever(statusBarStateController.currentOrUpcomingState) + .thenReturn(StatusBarState.SHADE) + setSplitShadeEnabled(true) + + controller.onPanelExpansionChanged(EXPANSION_EVENT) + + verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction) + } + + @Test fun onPanelExpansionChanged_inSplitShade_onKeyguard_setsFractionEqualToEventFraction() { whenever(statusBarStateController.currentOrUpcomingState) .thenReturn(StatusBarState.KEYGUARD) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt index cc45cf88fa18..2eca78a0412b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt @@ -57,7 +57,18 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.measure(50, 50) verify(mockTextAnimator).glyphFilter = any() - verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null) + verify(mockTextAnimator) + .setTextStyle( + weight = 300, + textSize = -1.0f, + color = 200, + strokeWidth = -1F, + animate = false, + duration = 350L, + interpolator = null, + delay = 0L, + onAnimationEnd = null + ) verifyNoMoreInteractions(mockTextAnimator) } @@ -68,8 +79,30 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.animateAppearOnLockscreen() verify(mockTextAnimator, times(2)).glyphFilter = any() - verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null) - verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null) + verify(mockTextAnimator) + .setTextStyle( + weight = 100, + textSize = -1.0f, + color = 200, + strokeWidth = -1F, + animate = false, + duration = 0L, + interpolator = null, + delay = 0L, + onAnimationEnd = null + ) + verify(mockTextAnimator) + .setTextStyle( + weight = 300, + textSize = -1.0f, + color = 200, + strokeWidth = -1F, + animate = true, + duration = 350L, + interpolator = null, + delay = 0L, + onAnimationEnd = null + ) verifyNoMoreInteractions(mockTextAnimator) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 9e23d548e5b5..957b0f10ec1f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -103,6 +103,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags(); fakeFeatureFlags.set(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE, true); + fakeFeatureFlags.set(Flags.SENSITIVE_REVEAL_ANIM, false); mNotificationTestHelper.setFeatureFlags(fakeFeatureFlags); } @@ -402,17 +403,6 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - public void testIsBlockingHelperShowing_isCorrectlyUpdated() throws Exception { - ExpandableNotificationRow group = mNotificationTestHelper.createGroup(); - - group.setBlockingHelperShowing(true); - assertTrue(group.isBlockingHelperShowing()); - - group.setBlockingHelperShowing(false); - assertFalse(group.isBlockingHelperShowing()); - } - - @Test public void testGetNumUniqueChildren_defaultChannel() throws Exception { ExpandableNotificationRow groupRow = mNotificationTestHelper.createGroup(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java index d7ac6b41ee78..3d8a74466a5c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java @@ -117,7 +117,6 @@ public class NotificationGutsManagerTest extends SysuiTestCase { @Mock private NotificationPresenter mPresenter; @Mock private NotificationActivityStarter mNotificationActivityStarter; @Mock private NotificationListContainer mNotificationListContainer; - @Mock private NotificationInfo.CheckSaveListener mCheckSaveListener; @Mock private OnSettingsClickListener mOnSettingsClickListener; @Mock private DeviceProvisionedController mDeviceProvisionedController; @Mock private CentralSurfaces mCentralSurfaces; @@ -173,7 +172,6 @@ public class NotificationGutsManagerTest extends SysuiTestCase { // Test doesn't support animation since the guts view is not attached. doNothing().when(guts).openControls( - eq(true) /* shouldDoCircularReveal */, anyInt(), anyInt(), anyBoolean(), @@ -190,7 +188,6 @@ public class NotificationGutsManagerTest extends SysuiTestCase { assertEquals(View.INVISIBLE, guts.getVisibility()); mTestableLooper.processAllMessages(); verify(guts).openControls( - eq(true), anyInt(), anyInt(), anyBoolean(), @@ -213,7 +210,6 @@ public class NotificationGutsManagerTest extends SysuiTestCase { // Test doesn't support animation since the guts view is not attached. doNothing().when(guts).openControls( - eq(true) /* shouldDoCircularReveal */, anyInt(), anyInt(), anyBoolean(), @@ -237,7 +233,6 @@ public class NotificationGutsManagerTest extends SysuiTestCase { assertTrue(mGutsManager.openGutsInternal(row, 0, 0, menuItem)); mTestableLooper.processAllMessages(); verify(guts).openControls( - eq(true), anyInt(), anyInt(), anyBoolean(), @@ -379,7 +374,6 @@ public class NotificationGutsManagerTest extends SysuiTestCase { public void testInitializeNotificationInfoView_PassesAlongProvisionedState() throws Exception { NotificationInfo notificationInfoView = mock(NotificationInfo.class); ExpandableNotificationRow row = spy(mHelper.createRow()); - row.setBlockingHelperShowing(false); modifyRanking(row.getEntry()) .setUserSentiment(USER_SENTIMENT_NEGATIVE) .build(); @@ -414,7 +408,6 @@ public class NotificationGutsManagerTest extends SysuiTestCase { public void testInitializeNotificationInfoView_withInitialAction() throws Exception { NotificationInfo notificationInfoView = mock(NotificationInfo.class); ExpandableNotificationRow row = spy(mHelper.createRow()); - row.setBlockingHelperShowing(true); modifyRanking(row.getEntry()) .setUserSentiment(USER_SENTIMENT_NEGATIVE) .build(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsTest.kt index e696c8738d72..fdfb4f4612fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsTest.kt @@ -76,7 +76,7 @@ class NotificationGutsTest : SysuiTestCase() { fun openControls() { guts.gutsContent = gutsContent - guts.openControls(true, 0, 0, false, null) + guts.openControls(0, 0, false, null) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt index 87f4c323b7cc..09382ec1945e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt @@ -20,6 +20,8 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.util.mockito.mock @@ -39,6 +41,8 @@ class AmbientStateTest : SysuiTestCase() { private val sectionProvider = StackScrollAlgorithm.SectionProvider { _, _ -> false } private val bypassController = StackScrollAlgorithm.BypassController { false } private val statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>() + private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>() + private val featureFlags = mock<FeatureFlags>() private lateinit var sut: AmbientState @@ -51,6 +55,8 @@ class AmbientStateTest : SysuiTestCase() { sectionProvider, bypassController, statusBarKeyguardViewManager, + largeScreenShadeInterpolator, + featureFlags ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index 9d759c4b6016..b1d3daa27eae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -7,6 +7,9 @@ import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.LegacySourceType @@ -21,7 +24,9 @@ import junit.framework.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever /** @@ -32,6 +37,9 @@ import org.mockito.Mockito.`when` as whenever @RunWithLooper class NotificationShelfTest : SysuiTestCase() { + @Mock private lateinit var largeScreenShadeInterpolator: LargeScreenShadeInterpolator + @Mock private lateinit var flags: FeatureFlags + private val shelf = NotificationShelf( context, /* attrs */ null, @@ -50,8 +58,12 @@ class NotificationShelfTest : SysuiTestCase() { @Before fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(ambientState.largeScreenShadeInterpolator).thenReturn(largeScreenShadeInterpolator) + whenever(ambientState.featureFlags).thenReturn(flags) shelf.bind(ambientState, /* hostLayoutController */ hostLayoutController) shelf.layout(/* left */ 0, /* top */ 0, /* right */ 30, /* bottom */5) + whenever(ambientState.isSmallScreen).thenReturn(true) } @Test @@ -295,7 +307,35 @@ class NotificationShelfTest : SysuiTestCase() { fun updateState_expansionChanging_shelfAlphaUpdated() { updateState_expansionChanging_shelfAlphaUpdated( expansionFraction = 0.6f, - expectedAlpha = ShadeInterpolation.getContentAlpha(0.6f) + expectedAlpha = ShadeInterpolation.getContentAlpha(0.6f), + ) + } + + @Test + fun updateState_flagTrue_largeScreen_expansionChanging_shelfAlphaUpdated_largeScreenValue() { + val expansionFraction = 0.6f + whenever(flags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)).thenReturn(true) + whenever(ambientState.isSmallScreen).thenReturn(false) + whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction)) + .thenReturn(0.123f) + + updateState_expansionChanging_shelfAlphaUpdated( + expansionFraction = expansionFraction, + expectedAlpha = 0.123f + ) + } + + @Test + fun updateState_flagFalse_largeScreen_expansionChanging_shelfAlphaUpdated_standardValue() { + val expansionFraction = 0.6f + whenever(flags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)).thenReturn(false) + whenever(ambientState.isSmallScreen).thenReturn(false) + whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction)) + .thenReturn(0.123f) + + updateState_expansionChanging_shelfAlphaUpdated( + expansionFraction = expansionFraction, + expectedAlpha = ShadeInterpolation.getContentAlpha(expansionFraction) ) } @@ -305,7 +345,17 @@ class NotificationShelfTest : SysuiTestCase() { updateState_expansionChanging_shelfAlphaUpdated( expansionFraction = 0.95f, - expectedAlpha = aboutToShowBouncerProgress(0.95f) + expectedAlpha = aboutToShowBouncerProgress(0.95f), + ) + } + + @Test + fun updateState_largeScreen_expansionChangingWhileBouncerInTransit_bouncerInterpolatorUsed() { + whenever(ambientState.isBouncerInTransit).thenReturn(true) + + updateState_expansionChanging_shelfAlphaUpdated( + expansionFraction = 0.95f, + expectedAlpha = aboutToShowBouncerProgress(0.95f), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index ff26a43c0006..45ae96c10345 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -52,7 +52,6 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeController; -import com.android.systemui.shade.transition.ShadeTransitionController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManager.UserChangedListener; @@ -135,7 +134,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private StackStateLogger mStackLogger; @Mock private NotificationStackScrollLogger mLogger; @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator; - @Mock private ShadeTransitionController mShadeTransitionController; @Mock private FeatureFlags mFeatureFlags; @Mock private NotificationTargetsHelper mNotificationTargetsHelper; @Mock private SecureSettings mSecureSettings; @@ -183,7 +181,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mNotifPipelineFlags, mNotifCollection, mLockscreenShadeTransitionController, - mShadeTransitionController, mUiEventLogger, mRemoteInputManager, mVisibilityLocationProviderDelegator, 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 cbf841b5a1f7..7153e59fff37 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 @@ -68,7 +68,9 @@ import com.android.systemui.ExpandHelper; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.NotificationShelfController; @@ -129,6 +131,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Mock private NotificationShelf mNotificationShelf; @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator; @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; + @Mock private FeatureFlags mFeatureFlags; @Before @UiThreadTest @@ -142,7 +146,10 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mDumpManager, mNotificationSectionsManager, mBypassController, - mStatusBarKeyguardViewManager)); + mStatusBarKeyguardViewManager, + mLargeScreenShadeInterpolator, + mFeatureFlags + )); // Inject dependencies before initializing the layout mDependency.injectTestDependency(SysuiStatusBarStateController.class, mBarState); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 4d9db8c28e07..7f20f1e53d97 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -8,6 +8,9 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ShadeInterpolation.getContentAlpha import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.EmptyShadeView import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarState @@ -15,11 +18,13 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.util.mockito.mock +import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.Mockito.any import org.mockito.Mockito.eq @@ -30,12 +35,19 @@ import org.mockito.Mockito.`when` as whenever @SmallTest class StackScrollAlgorithmTest : SysuiTestCase() { + + @JvmField @Rule + var expect: Expect = Expect.create() + + private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>() + private val hostView = FrameLayout(context) private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView) private val notificationRow = mock<ExpandableNotificationRow>() private val dumpManager = mock<DumpManager>() private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>() private val notificationShelf = mock<NotificationShelf>() + private val featureFlags = mock<FeatureFlags>() private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply { layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100) } @@ -44,8 +56,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { dumpManager, /* sectionProvider */ { _, _ -> false }, /* bypassController */ { false }, - mStatusBarKeyguardViewManager - ) + mStatusBarKeyguardViewManager, + largeScreenShadeInterpolator, + featureFlags, + ) private val testableResources = mContext.getOrCreateTestableResources() @@ -59,6 +73,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { fun setUp() { whenever(notificationShelf.viewState).thenReturn(ExpandableViewState()) whenever(notificationRow.viewState).thenReturn(ExpandableViewState()) + ambientState.isSmallScreen = true hostView.addView(notificationRow) } @@ -145,11 +160,46 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test - fun resetViewStates_expansionChangingWhileBouncerInTransit_notificationAlphaUpdated() { + fun resetViewStates_flagTrue_largeScreen_expansionChanging_alphaUpdated_largeScreenValue() { + val expansionFraction = 0.6f + val surfaceAlpha = 123f + ambientState.isSmallScreen = false + whenever(featureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) + .thenReturn(true) + whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false) + whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction)) + .thenReturn(surfaceAlpha) + + resetViewStates_expansionChanging_notificationAlphaUpdated( + expansionFraction = expansionFraction, + expectedAlpha = surfaceAlpha, + ) + } + + @Test + fun resetViewStates_flagFalse_largeScreen_expansionChanging_alphaUpdated_standardValue() { + val expansionFraction = 0.6f + val surfaceAlpha = 123f + ambientState.isSmallScreen = false + whenever(featureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) + .thenReturn(false) + whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false) + whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction)) + .thenReturn(surfaceAlpha) + + resetViewStates_expansionChanging_notificationAlphaUpdated( + expansionFraction = expansionFraction, + expectedAlpha = getContentAlpha(expansionFraction), + ) + } + + @Test + fun expansionChanging_largeScreen_bouncerInTransit_alphaUpdated_bouncerValues() { + ambientState.isSmallScreen = false whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true) resetViewStates_expansionChanging_notificationAlphaUpdated( expansionFraction = 0.95f, - expectedAlpha = aboutToShowBouncerProgress(0.95f) + expectedAlpha = aboutToShowBouncerProgress(0.95f), ) } @@ -696,7 +746,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { private fun resetViewStates_expansionChanging_notificationAlphaUpdated( expansionFraction: Float, - expectedAlpha: Float + expectedAlpha: Float, ) { ambientState.isExpansionChanging = true ambientState.expansionFraction = expansionFraction @@ -704,7 +754,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0) - assertThat(notificationRow.viewState.alpha).isEqualTo(expectedAlpha) + expect.that(notificationRow.viewState.alpha).isEqualTo(expectedAlpha) } } 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 5a5b1424758f..98f5a10aa203 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 @@ -1036,6 +1036,34 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test + public void testOccludingQSNotExpanded_transitionToAuthScrimmed() { + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); + + // GIVEN device occluded and panel is NOT expanded + mCentralSurfaces.setBarStateForTest(SHADE); // occluding on LS has StatusBarState = SHADE + when(mKeyguardStateController.isOccluded()).thenReturn(true); + mCentralSurfaces.mPanelExpanded = false; + + mCentralSurfaces.updateScrimController(); + + verify(mScrimController).transitionTo(eq(ScrimState.AUTH_SCRIMMED)); + } + + @Test + public void testOccludingQSExpanded_transitionToAuthScrimmedShade() { + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); + + // GIVEN device occluded and qs IS expanded + mCentralSurfaces.setBarStateForTest(SHADE); // occluding on LS has StatusBarState = SHADE + when(mKeyguardStateController.isOccluded()).thenReturn(true); + mCentralSurfaces.mPanelExpanded = true; + + mCentralSurfaces.updateScrimController(); + + verify(mScrimController).transitionTo(eq(ScrimState.AUTH_SCRIMMED_SHADE)); + } + + @Test public void testShowKeyguardImplementation_setsState() { when(mLockscreenUserManager.getCurrentProfiles()).thenReturn(new SparseArray<>()); @@ -1259,6 +1287,15 @@ public class CentralSurfacesImplTest extends SysuiTestCase { verify(mPowerManagerService, never()).wakeUp(anyLong(), anyInt(), anyString(), anyString()); } + @Test + public void frpLockedDevice_shadeDisabled() { + when(mDeviceProvisionedController.isFrpActive()).thenReturn(true); + when(mDozeServiceHost.isPulsing()).thenReturn(true); + mCentralSurfaces.updateNotificationPanelTouchState(); + + verify(mNotificationPanelViewController).setTouchAndAnimationDisabled(true); + } + /** * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard} * to reconfigure the keyguard to reflect the requested showing/occluded states. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index e1fba816382c..7a1270f3521d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -24,8 +24,6 @@ import static com.android.systemui.statusbar.phone.ScrimState.SHADE_LOCKED; import static com.google.common.truth.Truth.assertThat; -import static kotlinx.coroutines.flow.FlowKt.emptyFlow; - import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; @@ -41,6 +39,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.flow.FlowKt.emptyFlow; + import android.animation.Animator; import android.app.AlarmManager; import android.graphics.Color; @@ -59,6 +59,8 @@ import com.android.systemui.DejankUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.dock.DockManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; @@ -67,7 +69,8 @@ import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.scrim.ScrimView; -import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; +import com.android.systemui.shade.transition.LinearLargeScreenShadeInterpolator; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; @@ -104,6 +107,8 @@ public class ScrimControllerTest extends SysuiTestCase { private final FakeConfigurationController mConfigurationController = new FakeConfigurationController(); + private final LargeScreenShadeInterpolator + mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); private ScrimController mScrimController; private ScrimView mScrimBehind; @@ -128,11 +133,11 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @Mock private CoroutineDispatcher mMainDispatcher; - @Mock private SysuiStatusBarStateController mSysuiStatusBarStateController; // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + @Mock private FeatureFlags mFeatureFlags; private static class AnimatorListener implements Animator.AnimatorListener { private int mNumStarts; @@ -242,20 +247,28 @@ public class ScrimControllerTest extends SysuiTestCase { when(mKeyguardTransitionInteractor.getPrimaryBouncerToGoneTransition()) .thenReturn(emptyFlow()); - when(mPrimaryBouncerToGoneTransitionViewModel.getScrimBehindAlpha()) + when(mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha()) .thenReturn(emptyFlow()); - mScrimController = new ScrimController(mLightBarController, - mDozeParameters, mAlarmManager, mKeyguardStateController, mDelayedWakeLockBuilder, - new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor, - mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), + mScrimController = new ScrimController( + mLightBarController, + mDozeParameters, + mAlarmManager, + mKeyguardStateController, + mDelayedWakeLockBuilder, + new FakeHandler(mLooper.getLooper()), + mKeyguardUpdateMonitor, + mDockManager, + mConfigurationController, + new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, mStatusBarKeyguardViewManager, mPrimaryBouncerToGoneTransitionViewModel, mKeyguardTransitionInteractor, - mSysuiStatusBarStateController, - mMainDispatcher); + mMainDispatcher, + mLinearLargeScreenShadeInterpolator, + mFeatureFlags); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); @@ -653,7 +666,81 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - public void transitionToUnlocked() { + public void transitionToUnlocked_clippedQs() { + mScrimController.setClipsQsScrim(true); + mScrimController.setRawPanelExpansionFraction(0f); + mScrimController.transitionTo(ScrimState.UNLOCKED); + finishAnimationsImmediately(); + + assertScrimTinted(Map.of( + mNotificationsScrim, false, + mScrimInFront, false, + mScrimBehind, true + )); + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, TRANSPARENT, + mScrimBehind, OPAQUE)); + + mScrimController.setRawPanelExpansionFraction(0.25f); + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, SEMI_TRANSPARENT, + mScrimBehind, OPAQUE)); + + mScrimController.setRawPanelExpansionFraction(0.5f); + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, OPAQUE, + mScrimBehind, OPAQUE)); + } + + @Test + public void transitionToUnlocked_nonClippedQs_flagTrue_followsLargeScreensInterpolator() { + when(mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) + .thenReturn(true); + mScrimController.setClipsQsScrim(false); + mScrimController.setRawPanelExpansionFraction(0f); + mScrimController.transitionTo(ScrimState.UNLOCKED); + finishAnimationsImmediately(); + + assertScrimTinted(Map.of( + mNotificationsScrim, false, + mScrimInFront, false, + mScrimBehind, true + )); + // The large screens interpolator used in this test is a linear one, just for tests. + // Assertions below are based on this assumption, and that the code uses that interpolator + // when on a large screen (QS not clipped). + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, TRANSPARENT, + mScrimBehind, TRANSPARENT)); + + mScrimController.setRawPanelExpansionFraction(0.5f); + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, SEMI_TRANSPARENT, + mScrimBehind, SEMI_TRANSPARENT)); + + mScrimController.setRawPanelExpansionFraction(0.99f); + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, SEMI_TRANSPARENT, + mScrimBehind, SEMI_TRANSPARENT)); + + mScrimController.setRawPanelExpansionFraction(1f); + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, OPAQUE, + mScrimBehind, OPAQUE)); + } + + + @Test + public void transitionToUnlocked_nonClippedQs_flagFalse() { + when(mFeatureFlags.isEnabled(Flags.LARGE_SHADE_GRANULAR_ALPHA_INTERPOLATION)) + .thenReturn(false); mScrimController.setClipsQsScrim(false); mScrimController.setRawPanelExpansionFraction(0f); mScrimController.transitionTo(ScrimState.UNLOCKED); @@ -691,7 +778,6 @@ public class ScrimControllerTest extends SysuiTestCase { mScrimBehind, OPAQUE)); } - @Test public void scrimStateCallback() { mScrimController.transitionTo(ScrimState.UNLOCKED); @@ -879,17 +965,25 @@ public class ScrimControllerTest extends SysuiTestCase { // GIVEN display does NOT need blanking when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false); - mScrimController = new ScrimController(mLightBarController, - mDozeParameters, mAlarmManager, mKeyguardStateController, mDelayedWakeLockBuilder, - new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor, - mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()), + mScrimController = new ScrimController( + mLightBarController, + mDozeParameters, + mAlarmManager, + mKeyguardStateController, + mDelayedWakeLockBuilder, + new FakeHandler(mLooper.getLooper()), + mKeyguardUpdateMonitor, + mDockManager, + mConfigurationController, + new FakeExecutor(new FakeSystemClock()), mScreenOffAnimationController, mKeyguardUnlockAnimationController, mStatusBarKeyguardViewManager, mPrimaryBouncerToGoneTransitionViewModel, mKeyguardTransitionInteractor, - mSysuiStatusBarStateController, - mMainDispatcher); + mMainDispatcher, + mLinearLargeScreenShadeInterpolator, + mFeatureFlags); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 158e9adcff43..e2019b2814d9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -683,4 +683,30 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { // the following call before registering centralSurfaces should NOT throw a NPE: mStatusBarKeyguardViewManager.hideAlternateBouncer(true); } + + @Test + public void testResetHideBouncerWhenShowing_alternateBouncerHides() { + // GIVEN the keyguard is showing + reset(mAlternateBouncerInteractor); + when(mKeyguardStateController.isShowing()).thenReturn(true); + + // WHEN SBKV is reset with hideBouncerWhenShowing=true + mStatusBarKeyguardViewManager.reset(true); + + // THEN alternate bouncer is hidden + verify(mAlternateBouncerInteractor).hide(); + } + + @Test + public void testResetHideBouncerWhenShowingIsFalse_alternateBouncerHides() { + // GIVEN the keyguard is showing + reset(mAlternateBouncerInteractor); + when(mKeyguardStateController.isShowing()).thenReturn(true); + + // WHEN SBKV is reset with hideBouncerWhenShowing=false + mStatusBarKeyguardViewManager.reset(false); + + // THEN alternate bouncer is NOT hidden + verify(mAlternateBouncerInteractor, never()).hide(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index ccc57ad72f36..ee7e082c839f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.phone; import static android.service.notification.NotificationListenerService.REASON_CLICK; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + import static org.mockito.AdditionalAnswers.answerVoid; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -28,7 +31,6 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -38,6 +40,8 @@ import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; @@ -51,6 +55,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.NotificationVisibility; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.SysuiTestCase; @@ -90,9 +95,12 @@ import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -398,4 +406,40 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { // THEN display should try wake up for the full screen intent verify(mCentralSurfaces).wakeUpForFullScreenIntent(); } + + @Test + public void testOnFullScreenIntentWhenDozing_logToStatsd() { + final int kTestUid = 12345; + final String kTestActivityName = "TestActivity"; + // GIVEN entry that can has a full screen intent that can show + PendingIntent mockFullScreenIntent = mock(PendingIntent.class); + when(mockFullScreenIntent.getCreatorUid()).thenReturn(kTestUid); + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.name = kTestActivityName; + when(mockFullScreenIntent.queryIntentComponents(anyInt())) + .thenReturn(Arrays.asList(resolveInfo)); + Notification.Builder nb = new Notification.Builder(mContext, "a") + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setFullScreenIntent(mockFullScreenIntent, true); + StatusBarNotification sbn = new StatusBarNotification("pkg", "pkg", 0, + "tag" + System.currentTimeMillis(), 0, 0, + nb.build(), new UserHandle(0), null, 0); + NotificationEntry entry = mock(NotificationEntry.class); + when(entry.getImportance()).thenReturn(NotificationManager.IMPORTANCE_HIGH); + when(entry.getSbn()).thenReturn(sbn); + MockitoSession mockingSession = mockitoSession() + .mockStatic(FrameworkStatsLog.class) + .strictness(Strictness.LENIENT) + .startMocking(); + + // WHEN + mNotificationActivityStarter.launchFullScreenIntent(entry); + + // THEN the full screen intent should be logged to statsd. + verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.FULL_SCREEN_INTENT_LAUNCHED, + kTestUid, kTestActivityName)); + mockingSession.finishMocking(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt index 35dea60b1a1f..7c9351c8495d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/MobileInputLoggerTest.kt @@ -47,14 +47,14 @@ class MobileInputLoggerTest : SysuiTestCase() { val expectedNetId = NET_1_ID.toString() val expectedCaps = NET_1_CAPS.toString() - assertThat(actualString).contains("true") + assertThat(actualString).contains("onDefaultCapabilitiesChanged") assertThat(actualString).contains(expectedNetId) assertThat(actualString).contains(expectedCaps) } @Test fun testLogOnLost_bufferHasNetIdOfLostNetwork() { - logger.logOnLost(NET_1) + logger.logOnLost(NET_1, isDefaultNetworkCallback = false) val stringWriter = StringWriter() buffer.dump(PrintWriter(stringWriter), tailLength = 0) @@ -62,6 +62,7 @@ class MobileInputLoggerTest : SysuiTestCase() { val expectedNetId = NET_1_ID.toString() + assertThat(actualString).contains("onLost") assertThat(actualString).contains(expectedNetId) } 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 deleted file mode 100644 index 45189cf8d432..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt +++ /dev/null @@ -1,106 +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.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_IN -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION_OUT -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_IN, - connection.dataActivityDirection.hasActivityIn.toString(), - ) - ) - assertThat(logger.changes) - .contains( - Pair( - COL_ACTIVITY_DIRECTION_OUT, - connection.dataActivityDirection.hasActivityOut.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 53cd71f1bdf9..44fbd5b99894 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 @@ -17,9 +17,11 @@ 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.DataConnectionState 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.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository @@ -27,8 +29,19 @@ class FakeMobileConnectionRepository( override val subId: Int, override val tableLogBuffer: TableLogBuffer, ) : MobileConnectionRepository { - private val _connectionInfo = MutableStateFlow(MobileConnectionModel()) - override val connectionInfo = _connectionInfo + override val isEmergencyOnly = MutableStateFlow(false) + override val isRoaming = MutableStateFlow(false) + override val operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null) + override val isInService = MutableStateFlow(false) + override val isGsm = MutableStateFlow(false) + override val cdmaLevel = MutableStateFlow(0) + override val primaryLevel = MutableStateFlow(0) + override val dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected) + override val dataActivityDirection = + MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + override val carrierNetworkChangeActive = MutableStateFlow(false) + override val resolvedNetworkType: MutableStateFlow<ResolvedNetworkType> = + MutableStateFlow(ResolvedNetworkType.UnknownNetworkType) override val numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS) @@ -40,10 +53,6 @@ class FakeMobileConnectionRepository( override val networkName = MutableStateFlow<NetworkNameModel>(NetworkNameModel.Default("default")) - fun setConnectionInfo(model: MobileConnectionModel) { - _connectionInfo.value = model - } - fun setDataEnabled(enabled: Boolean) { _dataEnabled.value = enabled } 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 b072deedb9c9..37fac3458c83 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 @@ -25,7 +25,6 @@ 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 @@ -36,8 +35,11 @@ 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.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -123,34 +125,49 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC assertConnection(underTest, networkModel) } - private fun assertConnection( + private fun TestScope.startCollection(conn: DemoMobileConnectionRepository): Job { + val job = launch { + launch { conn.cdmaLevel.collect {} } + launch { conn.primaryLevel.collect {} } + launch { conn.dataActivityDirection.collect {} } + launch { conn.carrierNetworkChangeActive.collect {} } + launch { conn.isRoaming.collect {} } + launch { conn.networkName.collect {} } + launch { conn.isEmergencyOnly.collect {} } + launch { conn.dataConnectionState.collect {} } + } + return job + } + + private fun TestScope.assertConnection( conn: DemoMobileConnectionRepository, model: FakeNetworkEventModel ) { + val job = startCollection(underTest) when (model) { is FakeNetworkEventModel.Mobile -> { - val connectionInfo: MobileConnectionModel = conn.connectionInfo.value assertThat(conn.subId).isEqualTo(model.subId) - assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) - assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) - assertThat(connectionInfo.dataActivityDirection) + assertThat(conn.cdmaLevel.value).isEqualTo(model.level) + assertThat(conn.primaryLevel.value).isEqualTo(model.level) + assertThat(conn.dataActivityDirection.value) .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel()) - assertThat(connectionInfo.carrierNetworkChangeActive) + assertThat(conn.carrierNetworkChangeActive.value) .isEqualTo(model.carrierNetworkChange) - assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming) + assertThat(conn.isRoaming.value).isEqualTo(model.roaming) assertThat(conn.networkName.value) .isEqualTo(NetworkNameModel.IntentDerived(model.name)) // TODO(b/261029387): check these once we start handling them - assertThat(connectionInfo.isEmergencyOnly).isFalse() - assertThat(connectionInfo.isGsm).isFalse() - assertThat(connectionInfo.dataConnectionState) - .isEqualTo(DataConnectionState.Connected) + assertThat(conn.isEmergencyOnly.value).isFalse() + assertThat(conn.isGsm.value).isFalse() + assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected) } // MobileDisabled isn't combinatorial in nature, and is tested in // DemoMobileConnectionsRepositoryTest.kt else -> {} } + + job.cancel() } /** Matches [FakeNetworkEventModel] */ 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 f60d92bde202..0e45d8ea5563 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 @@ -26,7 +26,6 @@ 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 @@ -40,9 +39,11 @@ import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import junit.framework.Assert import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -524,47 +525,65 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } - private fun assertConnection( + private fun TestScope.startCollection(conn: DemoMobileConnectionRepository): Job { + val job = launch { + launch { conn.cdmaLevel.collect {} } + launch { conn.primaryLevel.collect {} } + launch { conn.dataActivityDirection.collect {} } + launch { conn.carrierNetworkChangeActive.collect {} } + launch { conn.isRoaming.collect {} } + launch { conn.networkName.collect {} } + launch { conn.isEmergencyOnly.collect {} } + launch { conn.dataConnectionState.collect {} } + } + return job + } + + private fun TestScope.assertConnection( conn: DemoMobileConnectionRepository, - model: FakeNetworkEventModel + model: FakeNetworkEventModel, ) { + val job = startCollection(conn) + // Assert the fields using the `MutableStateFlow` so that we don't have to start up + // a collector for every field for every test when (model) { is FakeNetworkEventModel.Mobile -> { - val connectionInfo: MobileConnectionModel = conn.connectionInfo.value assertThat(conn.subId).isEqualTo(model.subId) - assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) - assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) - assertThat(connectionInfo.dataActivityDirection) + assertThat(conn.cdmaLevel.value).isEqualTo(model.level) + assertThat(conn.primaryLevel.value).isEqualTo(model.level) + assertThat(conn.dataActivityDirection.value) .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel()) - assertThat(connectionInfo.carrierNetworkChangeActive) + assertThat(conn.carrierNetworkChangeActive.value) .isEqualTo(model.carrierNetworkChange) - assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming) + assertThat(conn.isRoaming.value).isEqualTo(model.roaming) assertThat(conn.networkName.value) .isEqualTo(NetworkNameModel.IntentDerived(model.name)) // TODO(b/261029387) check these once we start handling them - assertThat(connectionInfo.isEmergencyOnly).isFalse() - assertThat(connectionInfo.isGsm).isFalse() - assertThat(connectionInfo.dataConnectionState) - .isEqualTo(DataConnectionState.Connected) + assertThat(conn.isEmergencyOnly.value).isFalse() + assertThat(conn.isGsm.value).isFalse() + assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected) } else -> {} } + + job.cancel() } - private fun assertCarrierMergedConnection( + private fun TestScope.assertCarrierMergedConnection( conn: DemoMobileConnectionRepository, model: FakeWifiEventModel.CarrierMerged, ) { - val connectionInfo: MobileConnectionModel = conn.connectionInfo.value + val job = startCollection(conn) assertThat(conn.subId).isEqualTo(model.subscriptionId) - assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) - assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) - assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false) - assertThat(connectionInfo.isRoaming).isEqualTo(false) - assertThat(connectionInfo.isEmergencyOnly).isFalse() - assertThat(connectionInfo.isGsm).isFalse() - assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected) + assertThat(conn.cdmaLevel.value).isEqualTo(model.level) + assertThat(conn.primaryLevel.value).isEqualTo(model.level) + assertThat(conn.carrierNetworkChangeActive.value).isEqualTo(false) + assertThat(conn.isRoaming.value).isEqualTo(false) + assertThat(conn.isEmergencyOnly.value).isFalse() + assertThat(conn.isGsm.value).isFalse() + assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected) + job.cancel() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt index f0f213bc0d58..441186acb6b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt @@ -22,7 +22,6 @@ 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 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel @@ -75,36 +74,48 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { } @Test - fun connectionInfo_inactiveWifi_isDefault() = + fun inactiveWifi_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestConnState: DataConnectionState? = null + var latestNetType: ResolvedNetworkType? = null + + val dataJob = + underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this) + val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this) wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) - assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected) + assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) - job.cancel() + dataJob.cancel() + netJob.cancel() } @Test - fun connectionInfo_activeWifi_isDefault() = + fun activeWifi_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestConnState: DataConnectionState? = null + var latestNetType: ResolvedNetworkType? = null + + val dataJob = + underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this) + val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this) wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1)) - assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected) + assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) - job.cancel() + dataJob.cancel() + netJob.cancel() } @Test - fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = + fun carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Int? = null + val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) wifiRepository.setIsWifiEnabled(true) wifiRepository.setIsWifiDefault(true) @@ -117,34 +128,16 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) ) - val expected = - MobileConnectionModel( - primaryLevel = 3, - cdmaLevel = 3, - dataConnectionState = DataConnectionState.Connected, - dataActivityDirection = - DataActivityModel( - hasActivityIn = false, - hasActivityOut = false, - ), - resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, - isRoaming = false, - isEmergencyOnly = false, - operatorAlphaShort = null, - isInService = true, - isGsm = false, - carrierNetworkChangeActive = false, - ) - assertThat(latest).isEqualTo(expected) + assertThat(latest).isEqualTo(3) job.cancel() } @Test - fun connectionInfo_activity_comesFromWifiActivity() = + fun activity_comesFromWifiActivity() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataActivityModel? = null + val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) wifiRepository.setIsWifiEnabled(true) wifiRepository.setIsWifiDefault(true) @@ -162,8 +155,8 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) ) - assertThat(latest!!.dataActivityDirection.hasActivityIn).isTrue() - assertThat(latest!!.dataActivityDirection.hasActivityOut).isFalse() + assertThat(latest!!.hasActivityIn).isTrue() + assertThat(latest!!.hasActivityOut).isFalse() wifiRepository.setWifiActivity( DataActivityModel( @@ -172,17 +165,19 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) ) - assertThat(latest!!.dataActivityDirection.hasActivityIn).isFalse() - assertThat(latest!!.dataActivityDirection.hasActivityOut).isTrue() + assertThat(latest!!.hasActivityIn).isFalse() + assertThat(latest!!.hasActivityOut).isTrue() job.cancel() } @Test - fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() = + fun carrierMergedWifi_wrongSubId_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestLevel: Int? = null + var latestType: ResolvedNetworkType? = null + val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) + val typeJob = underTest.resolvedNetworkType.onEach { latestType = it }.launchIn(this) wifiRepository.setWifiNetwork( WifiNetworkModel.CarrierMerged( @@ -192,20 +187,19 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) ) - assertThat(latest).isEqualTo(MobileConnectionModel()) - assertThat(latest!!.primaryLevel).isNotEqualTo(3) - assertThat(latest!!.resolvedNetworkType) - .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) + assertThat(latestLevel).isNotEqualTo(3) + assertThat(latestType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) - job.cancel() + levelJob.cancel() + typeJob.cancel() } // This scenario likely isn't possible, but write a test for it anyway @Test - fun connectionInfo_carrierMergedButNotEnabled_isDefault() = + fun carrierMergedButNotEnabled_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Int? = null + val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) wifiRepository.setWifiNetwork( WifiNetworkModel.CarrierMerged( @@ -216,17 +210,17 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) wifiRepository.setIsWifiEnabled(false) - assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latest).isNotEqualTo(3) job.cancel() } // This scenario likely isn't possible, but write a test for it anyway @Test - fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() = + fun carrierMergedButWifiNotDefault_isDefault() = testScope.runTest { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Int? = null + val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) wifiRepository.setWifiNetwork( WifiNetworkModel.CarrierMerged( @@ -237,7 +231,7 @@ class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { ) wifiRepository.setIsWifiDefault(false) - assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latest).isNotEqualTo(3) job.cancel() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt index cd4d8472763f..db5a7d1ad84a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt @@ -24,13 +24,12 @@ import androidx.test.filters.SmallTest 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.MobileConnectionModel -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_OPERATOR -import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel @@ -94,16 +93,16 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { @Test fun startingIsCarrierMerged_usesCarrierMergedInitially() = testScope.runTest { - val carrierMergedConnectionInfo = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator", - ) - carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo) + val carrierMergedOperatorName = "Carrier Merged Operator" + val nonCarrierMergedName = "Non-carrier-merged" + + carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperatorName + mobileRepo.operatorAlphaShort.value = nonCarrierMergedName initializeRepo(startingIsCarrierMerged = true) assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo) - assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo) + assertThat(underTest.operatorAlphaShort.value).isEqualTo(carrierMergedOperatorName) verify(mobileFactory, never()) .build( SUB_ID, @@ -116,16 +115,16 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { @Test fun startingNotCarrierMerged_usesTypicalInitially() = testScope.runTest { - val mobileConnectionInfo = - MobileConnectionModel( - operatorAlphaShort = "Typical Operator", - ) - mobileRepo.setConnectionInfo(mobileConnectionInfo) + val carrierMergedOperatorName = "Carrier Merged Operator" + val nonCarrierMergedName = "Typical Operator" + + carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperatorName + mobileRepo.operatorAlphaShort.value = nonCarrierMergedName initializeRepo(startingIsCarrierMerged = false) assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo) - assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo) + assertThat(underTest.operatorAlphaShort.value).isEqualTo(nonCarrierMergedName) verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer) } @@ -156,39 +155,40 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { testScope.runTest { initializeRepo(startingIsCarrierMerged = false) - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestName: String? = null + var latestLevel: Int? = null + + val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) + val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) underTest.setIsCarrierMerged(true) - val info1 = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator", - primaryLevel = 1, - ) - carrierMergedRepo.setConnectionInfo(info1) + val operator1 = "Carrier Merged Operator" + val level1 = 1 + carrierMergedRepo.operatorAlphaShort.value = operator1 + carrierMergedRepo.primaryLevel.value = level1 - assertThat(latest).isEqualTo(info1) + assertThat(latestName).isEqualTo(operator1) + assertThat(latestLevel).isEqualTo(level1) - val info2 = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator #2", - primaryLevel = 2, - ) - carrierMergedRepo.setConnectionInfo(info2) + val operator2 = "Carrier Merged Operator #2" + val level2 = 2 + carrierMergedRepo.operatorAlphaShort.value = operator2 + carrierMergedRepo.primaryLevel.value = level2 - assertThat(latest).isEqualTo(info2) + assertThat(latestName).isEqualTo(operator2) + assertThat(latestLevel).isEqualTo(level2) - val info3 = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator #3", - primaryLevel = 3, - ) - carrierMergedRepo.setConnectionInfo(info3) + val operator3 = "Carrier Merged Operator #3" + val level3 = 3 + carrierMergedRepo.operatorAlphaShort.value = operator3 + carrierMergedRepo.primaryLevel.value = level3 - assertThat(latest).isEqualTo(info3) + assertThat(latestName).isEqualTo(operator3) + assertThat(latestLevel).isEqualTo(level3) - job.cancel() + nameJob.cancel() + levelJob.cancel() } @Test @@ -196,39 +196,40 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { testScope.runTest { initializeRepo(startingIsCarrierMerged = false) - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestName: String? = null + var latestLevel: Int? = null + + val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) + val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) underTest.setIsCarrierMerged(false) - val info1 = - MobileConnectionModel( - operatorAlphaShort = "Typical Merged Operator", - primaryLevel = 1, - ) - mobileRepo.setConnectionInfo(info1) + val operator1 = "Typical Merged Operator" + val level1 = 1 + mobileRepo.operatorAlphaShort.value = operator1 + mobileRepo.primaryLevel.value = level1 - assertThat(latest).isEqualTo(info1) + assertThat(latestName).isEqualTo(operator1) + assertThat(latestLevel).isEqualTo(level1) - val info2 = - MobileConnectionModel( - operatorAlphaShort = "Typical Merged Operator #2", - primaryLevel = 2, - ) - mobileRepo.setConnectionInfo(info2) + val operator2 = "Typical Merged Operator #2" + val level2 = 2 + mobileRepo.operatorAlphaShort.value = operator2 + mobileRepo.primaryLevel.value = level2 - assertThat(latest).isEqualTo(info2) + assertThat(latestName).isEqualTo(operator2) + assertThat(latestLevel).isEqualTo(level2) - val info3 = - MobileConnectionModel( - operatorAlphaShort = "Typical Merged Operator #3", - primaryLevel = 3, - ) - mobileRepo.setConnectionInfo(info3) + val operator3 = "Typical Merged Operator #3" + val level3 = 3 + mobileRepo.operatorAlphaShort.value = operator3 + mobileRepo.primaryLevel.value = level3 - assertThat(latest).isEqualTo(info3) + assertThat(latestName).isEqualTo(operator3) + assertThat(latestLevel).isEqualTo(level3) - job.cancel() + nameJob.cancel() + levelJob.cancel() } @Test @@ -236,57 +237,58 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { testScope.runTest { initializeRepo(startingIsCarrierMerged = false) - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latestName: String? = null + var latestLevel: Int? = null - val carrierMergedInfo = - MobileConnectionModel( - operatorAlphaShort = "Carrier Merged Operator", - primaryLevel = 4, - ) - carrierMergedRepo.setConnectionInfo(carrierMergedInfo) + val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) + val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) - val mobileInfo = - MobileConnectionModel( - operatorAlphaShort = "Typical Operator", - primaryLevel = 2, - ) - mobileRepo.setConnectionInfo(mobileInfo) + val carrierMergedOperator = "Carrier Merged Operator" + val carrierMergedLevel = 4 + carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperator + carrierMergedRepo.primaryLevel.value = carrierMergedLevel + + val mobileName = "Typical Operator" + val mobileLevel = 2 + mobileRepo.operatorAlphaShort.value = mobileName + mobileRepo.primaryLevel.value = mobileLevel // Start with the mobile info - assertThat(latest).isEqualTo(mobileInfo) + assertThat(latestName).isEqualTo(mobileName) + assertThat(latestLevel).isEqualTo(mobileLevel) // WHEN isCarrierMerged is set to true underTest.setIsCarrierMerged(true) // THEN the carrier merged info is used - assertThat(latest).isEqualTo(carrierMergedInfo) + assertThat(latestName).isEqualTo(carrierMergedOperator) + assertThat(latestLevel).isEqualTo(carrierMergedLevel) - val newCarrierMergedInfo = - MobileConnectionModel( - operatorAlphaShort = "New CM Operator", - primaryLevel = 0, - ) - carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo) + val newCarrierMergedName = "New CM Operator" + val newCarrierMergedLevel = 0 + carrierMergedRepo.operatorAlphaShort.value = newCarrierMergedName + carrierMergedRepo.primaryLevel.value = newCarrierMergedLevel - assertThat(latest).isEqualTo(newCarrierMergedInfo) + assertThat(latestName).isEqualTo(newCarrierMergedName) + assertThat(latestLevel).isEqualTo(newCarrierMergedLevel) // WHEN isCarrierMerged is set to false underTest.setIsCarrierMerged(false) // THEN the typical info is used - assertThat(latest).isEqualTo(mobileInfo) + assertThat(latestName).isEqualTo(mobileName) + assertThat(latestLevel).isEqualTo(mobileLevel) - val newMobileInfo = - MobileConnectionModel( - operatorAlphaShort = "New Mobile Operator", - primaryLevel = 3, - ) - mobileRepo.setConnectionInfo(newMobileInfo) + val newMobileName = "New MobileOperator" + val newMobileLevel = 3 + mobileRepo.operatorAlphaShort.value = newMobileName + mobileRepo.primaryLevel.value = newMobileLevel - assertThat(latest).isEqualTo(newMobileInfo) + assertThat(latestName).isEqualTo(newMobileName) + assertThat(latestLevel).isEqualTo(newMobileLevel) - job.cancel() + nameJob.cancel() + levelJob.cancel() } @Test @@ -370,7 +372,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { initializeRepo(startingIsCarrierMerged = false) - val job = underTest.connectionInfo.launchIn(this) + val emergencyJob = underTest.isEmergencyOnly.launchIn(this) + val operatorJob = underTest.operatorAlphaShort.launchIn(this) // WHEN we set up some mobile connection info val serviceState = ServiceState() @@ -394,7 +397,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpDiff") assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}true") - job.cancel() + emergencyJob.cancel() + operatorJob.cancel() } @Test @@ -409,7 +413,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { initializeRepo(startingIsCarrierMerged = true) - val job = underTest.connectionInfo.launchIn(this) + val job = underTest.primaryLevel.launchIn(this) // WHEN we set up carrier merged info val networkId = 2 @@ -452,7 +456,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { initializeRepo(startingIsCarrierMerged = false) - val job = underTest.connectionInfo.launchIn(this) + val job = underTest.primaryLevel.launchIn(this) // WHEN we set up some mobile connection info val signalStrength = mock<SignalStrength>() @@ -502,12 +506,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { assertThat(bufferAfterCarrierMerged).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1") // WHEN the normal network is updated - val newMobileInfo = - MobileConnectionModel( - operatorAlphaShort = "Mobile Operator 2", - primaryLevel = 0, - ) - mobileRepo.setConnectionInfo(newMobileInfo) + mobileRepo.primaryLevel.value = 0 // THEN the new level is logged assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}0") @@ -529,7 +528,7 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { // WHEN isCarrierMerged = false initializeRepo(startingIsCarrierMerged = false) - val job = underTest.connectionInfo.launchIn(this) + val job = underTest.primaryLevel.launchIn(this) val signalStrength = mock<SignalStrength>() whenever(signalStrength.level).thenReturn(1) 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 bd5a4d7f7385..f6e595924f58 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 @@ -50,13 +50,15 @@ 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.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger 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 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 @@ -135,235 +137,285 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test - fun testFlowForSubId_default() = + fun emergencyOnly() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) - - assertThat(latest).isEqualTo(MobileConnectionModel()) - - job.cancel() - } - - @Test - fun testFlowForSubId_emergencyOnly() = - runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this) val serviceState = ServiceState() serviceState.isEmergencyOnly = true getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) - assertThat(latest?.isEmergencyOnly).isEqualTo(true) + assertThat(latest).isEqualTo(true) job.cancel() } @Test - fun testFlowForSubId_emergencyOnly_toggles() = + fun emergencyOnly_toggles() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<ServiceStateListener>() val serviceState = ServiceState() serviceState.isEmergencyOnly = true callback.onServiceStateChanged(serviceState) + assertThat(latest).isTrue() + serviceState.isEmergencyOnly = false callback.onServiceStateChanged(serviceState) - assertThat(latest?.isEmergencyOnly).isEqualTo(false) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun cdmaLevelUpdates() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.cdmaLevel.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest).isEqualTo(2) + + // gsmLevel updates, no change to cdmaLevel + strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) + + assertThat(latest).isEqualTo(2) job.cancel() } @Test - fun testFlowForSubId_signalStrengths_levelsUpdate() = + fun gsmLevelUpdates() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Int? = null + val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() - val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) callback.onSignalStrengthsChanged(strength) - assertThat(latest?.isGsm).isEqualTo(true) - assertThat(latest?.primaryLevel).isEqualTo(1) - assertThat(latest?.cdmaLevel).isEqualTo(2) + assertThat(latest).isEqualTo(1) + + strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest).isEqualTo(3) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_connected() = + fun isGsm() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.isGsm.onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest).isTrue() + + strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = false) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun dataConnectionState_connected() = + runBlocking(IMMEDIATE) { + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_CONNECTED, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Connected) + assertThat(latest).isEqualTo(DataConnectionState.Connected) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_connecting() = + fun dataConnectionState_connecting() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_CONNECTING, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Connecting) + assertThat(latest).isEqualTo(DataConnectionState.Connecting) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_disconnected() = + fun dataConnectionState_disconnected() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_DISCONNECTED, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Disconnected) + assertThat(latest).isEqualTo(DataConnectionState.Disconnected) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_disconnecting() = + fun dataConnectionState_disconnecting() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_DISCONNECTING, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Disconnecting) + assertThat(latest).isEqualTo(DataConnectionState.Disconnecting) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_suspended() = + fun dataConnectionState_suspended() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Suspended) + assertThat(latest).isEqualTo(DataConnectionState.Suspended) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_handoverInProgress() = + fun dataConnectionState_handoverInProgress() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */) - assertThat(latest?.dataConnectionState) - .isEqualTo(DataConnectionState.HandoverInProgress) + assertThat(latest).isEqualTo(DataConnectionState.HandoverInProgress) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_unknown() = + fun dataConnectionState_unknown() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Unknown) + assertThat(latest).isEqualTo(DataConnectionState.Unknown) job.cancel() } @Test - fun testFlowForSubId_dataConnectionState_invalid() = + fun dataConnectionState_invalid() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataConnectionState? = null + val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() callback.onDataConnectionStateChanged(45, 200 /* unused */) - assertThat(latest?.dataConnectionState).isEqualTo(DataConnectionState.Invalid) + assertThat(latest).isEqualTo(DataConnectionState.Invalid) job.cancel() } @Test - fun testFlowForSubId_dataActivity() = + fun dataActivity() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: DataActivityModel? = null + val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<DataActivityListener>() callback.onDataActivity(DATA_ACTIVITY_INOUT) - assertThat(latest?.dataActivityDirection) - .isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel()) + assertThat(latest).isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel()) job.cancel() } @Test - fun testFlowForSubId_carrierNetworkChange() = + fun carrierNetworkChange() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: Boolean? = null + val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>() callback.onCarrierNetworkChange(true) - assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true) + assertThat(latest).isEqualTo(true) + + job.cancel() + } + + @Test + fun networkType_default() = + runBlocking(IMMEDIATE) { + var latest: ResolvedNetworkType? = null + val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) + + val expected = UnknownNetworkType + + assertThat(latest).isEqualTo(expected) job.cancel() } @Test - fun subscriptionFlow_networkType_default() = + fun networkType_unknown_hasCorrectKey() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: ResolvedNetworkType? = null + val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) + val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() + val type = NETWORK_TYPE_UNKNOWN val expected = UnknownNetworkType + val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) } + callback.onDisplayInfoChanged(ti) - assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + assertThat(latest).isEqualTo(expected) + assertThat(latest!!.lookupKey).isEqualTo(MobileMappings.toIconKey(type)) job.cancel() } @Test - fun subscriptionFlow_networkType_updatesUsingDefault() = + fun networkType_updatesUsingDefault() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: ResolvedNetworkType? = null + val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() val type = NETWORK_TYPE_LTE @@ -371,16 +423,16 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) } callback.onDisplayInfoChanged(ti) - assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + assertThat(latest).isEqualTo(expected) job.cancel() } @Test - fun subscriptionFlow_networkType_updatesUsingOverride() = + fun networkType_updatesUsingOverride() = runBlocking(IMMEDIATE) { - var latest: MobileConnectionModel? = null - val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + var latest: ResolvedNetworkType? = null + val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() val type = OVERRIDE_NETWORK_TYPE_LTE_CA @@ -392,7 +444,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } callback.onDisplayInfoChanged(ti) - assertThat(latest?.resolvedNetworkType).isEqualTo(expected) + assertThat(latest).isEqualTo(expected) job.cancel() } @@ -466,7 +518,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun `roaming - gsm - queries service state`() = runBlocking(IMMEDIATE) { var latest: Boolean? = null - val job = underTest.connectionInfo.onEach { latest = it.isRoaming }.launchIn(this) + val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) val serviceState = ServiceState() serviceState.roaming = false @@ -492,8 +544,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun `activity - updates from callback`() = runBlocking(IMMEDIATE) { var latest: DataActivityModel? = null - val job = - underTest.connectionInfo.onEach { latest = it.dataActivityDirection }.launchIn(this) + val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) assertThat(latest) .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) @@ -611,8 +662,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { runBlocking(IMMEDIATE) { var latest: String? = null - val job = - underTest.connectionInfo.onEach { latest = it.operatorAlphaShort }.launchIn(this) + val job = underTest.operatorAlphaShort.onEach { latest = it }.launchIn(this) val shortName = "short name" val serviceState = ServiceState() @@ -633,7 +683,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun `connection model - isInService - not iwlan`() = runBlocking(IMMEDIATE) { var latest: Boolean? = null - val job = underTest.connectionInfo.onEach { latest = it.isInService }.launchIn(this) + val job = underTest.isInService.onEach { latest = it }.launchIn(this) val serviceState = ServiceState() serviceState.voiceRegState = STATE_IN_SERVICE @@ -658,7 +708,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun `connection model - isInService - is iwlan - voice out of service - data in service`() = runBlocking(IMMEDIATE) { var latest: Boolean? = null - val job = underTest.connectionInfo.onEach { latest = it.isInService }.launchIn(this) + val job = underTest.isInService.onEach { latest = it }.launchIn(this) // Mock the service state here so we can make it specifically IWLAN val serviceState: ServiceState = mock() 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 fa072fc366eb..1eb1056204cd 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 @@ -23,7 +23,6 @@ import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons 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.CarrierMergedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType @@ -74,9 +73,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_level_default_unknown() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel(isGsm = true), - ) + connectionRepository.isGsm.value = true var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -89,13 +86,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_usesGsmLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL - ), - ) + connectionRepository.isGsm.value = true + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -108,13 +101,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL, - ), - ) + connectionRepository.isGsm.value = true + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = true var latest: Int? = null @@ -128,9 +117,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_level_default_unknown() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel(isGsm = false), - ) + connectionRepository.isGsm.value = false var latest: Int? = null val job = underTest.level.onEach { latest = it }.launchIn(this) @@ -142,13 +129,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL - ), - ) + connectionRepository.isGsm.value = false + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = true var latest: Int? = null @@ -162,13 +145,9 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - primaryLevel = GSM_LEVEL, - cdmaLevel = CDMA_LEVEL, - ), - ) + connectionRepository.isGsm.value = false + connectionRepository.primaryLevel.value = GSM_LEVEL + connectionRepository.cdmaLevel.value = CDMA_LEVEL mobileIconsInteractor.alwaysUseCdmaLevel.value = false var latest: Int? = null @@ -197,11 +176,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_three_g() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -214,23 +190,14 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_updates_on_change() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - DefaultNetworkType( - mobileMappingsProxy.toIconKey(FOUR_G), - ), - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G)) yield() assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G) @@ -241,12 +208,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_5g_override_type() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) - ), - ) + connectionRepository.resolvedNetworkType.value = + OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -259,12 +222,8 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_default_if_no_lookup() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = - DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)), - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -277,11 +236,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun iconGroup_carrierMerged_usesOverride() = runBlocking(IMMEDIATE) { - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = CarrierMergedNetworkType, - ), - ) + connectionRepository.resolvedNetworkType.value = CarrierMergedNetworkType var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -295,11 +250,8 @@ class MobileIconInteractorTest : SysuiTestCase() { fun `icon group - checks default data`() = runBlocking(IMMEDIATE) { mobileIconsInteractor.defaultDataSubId.value = SUB_1_ID - connectionRepository.setConnectionInfo( - MobileConnectionModel( - resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) - ), - ) + connectionRepository.resolvedNetworkType.value = + DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) var latest: MobileIconGroup? = null val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) @@ -380,9 +332,7 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel(dataConnectionState = DataConnectionState.Connected) - ) + connectionRepository.dataConnectionState.value = DataConnectionState.Connected yield() assertThat(latest).isTrue() @@ -396,9 +346,7 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo( - MobileConnectionModel(dataConnectionState = DataConnectionState.Disconnected) - ) + connectionRepository.dataConnectionState.value = DataConnectionState.Disconnected assertThat(latest).isFalse() @@ -411,11 +359,11 @@ class MobileIconInteractorTest : SysuiTestCase() { var latest: Boolean? = null val job = underTest.isInService.onEach { latest = it }.launchIn(this) - connectionRepository.setConnectionInfo(MobileConnectionModel(isInService = true)) + connectionRepository.isInService.value = true assertThat(latest).isTrue() - connectionRepository.setConnectionInfo(MobileConnectionModel(isInService = false)) + connectionRepository.isInService.value = false assertThat(latest).isFalse() @@ -429,22 +377,13 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = false, - ) - ) + connectionRepository.isGsm.value = true + connectionRepository.isRoaming.value = false yield() assertThat(latest).isFalse() - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = true, - ) - ) + connectionRepository.isRoaming.value = true yield() assertThat(latest).isTrue() @@ -459,23 +398,15 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = false - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = true, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = true yield() assertThat(latest).isFalse() connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = false, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = false yield() assertThat(latest).isTrue() @@ -490,25 +421,15 @@ class MobileIconInteractorTest : SysuiTestCase() { val job = underTest.isRoaming.onEach { latest = it }.launchIn(this) connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = false, - isRoaming = true, - carrierNetworkChangeActive = true, - ) - ) + connectionRepository.isGsm.value = false + connectionRepository.isRoaming.value = true + connectionRepository.carrierNetworkChangeActive.value = true yield() assertThat(latest).isFalse() connectionRepository.cdmaRoaming.value = true - connectionRepository.setConnectionInfo( - MobileConnectionModel( - isGsm = true, - isRoaming = true, - carrierNetworkChangeActive = true, - ) - ) + connectionRepository.isGsm.value = true yield() assertThat(latest).isFalse() @@ -526,24 +447,20 @@ class MobileIconInteractorTest : SysuiTestCase() { // Default network name, operator name is non-null, uses the operator name connectionRepository.networkName.value = DEFAULT_NAME - connectionRepository.setConnectionInfo( - MobileConnectionModel(operatorAlphaShort = testOperatorName) - ) + connectionRepository.operatorAlphaShort.value = testOperatorName yield() assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName)) // Default network name, operator name is null, uses the default - connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null)) + connectionRepository.operatorAlphaShort.value = 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) - ) + connectionRepository.operatorAlphaShort.value = testOperatorName yield() assertThat(latest).isEqualTo(DERIVED_NAME) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt index 5129f8584165..6980a0b4565e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt @@ -90,6 +90,12 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { } @Test + fun testFrpNotActiveByDefault() { + init() + assertThat(controller.isFrpActive).isFalse() + } + + @Test fun testNotUserSetupByDefault() { init() assertThat(controller.isUserSetup(START_USER)).isFalse() @@ -104,6 +110,14 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { } @Test + fun testFrpActiveWhenCreated() { + settings.putInt(Settings.Secure.SECURE_FRP_MODE, 1) + init() + + assertThat(controller.isFrpActive).isTrue() + } + + @Test fun testUserSetupWhenCreated() { settings.putIntForUser(Settings.Secure.USER_SETUP_COMPLETE, 1, START_USER) init() @@ -122,6 +136,16 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { } @Test + fun testFrpActiveChange() { + init() + + settings.putInt(Settings.Secure.SECURE_FRP_MODE, 1) + testableLooper.processAllMessages() // background observer + + assertThat(controller.isFrpActive).isTrue() + } + + @Test fun testUserSetupChange() { init() @@ -164,6 +188,7 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { mainExecutor.runAllReady() verify(listener, never()).onDeviceProvisionedChanged() + verify(listener, never()).onFrpActiveChanged() verify(listener, never()).onUserSetupChanged() verify(listener, never()).onUserSwitched() } @@ -181,6 +206,7 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { verify(listener).onUserSwitched() verify(listener, never()).onUserSetupChanged() verify(listener, never()).onDeviceProvisionedChanged() + verify(listener, never()).onFrpActiveChanged() } @Test @@ -195,6 +221,7 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { verify(listener, never()).onUserSwitched() verify(listener).onUserSetupChanged() verify(listener, never()).onDeviceProvisionedChanged() + verify(listener, never()).onFrpActiveChanged() } @Test @@ -208,10 +235,26 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { verify(listener, never()).onUserSwitched() verify(listener, never()).onUserSetupChanged() + verify(listener, never()).onFrpActiveChanged() verify(listener).onDeviceProvisionedChanged() } @Test + fun testListenerCalledOnFrpActiveChanged() { + init() + controller.addCallback(listener) + + settings.putInt(Settings.Secure.SECURE_FRP_MODE, 1) + testableLooper.processAllMessages() + mainExecutor.runAllReady() + + verify(listener, never()).onUserSwitched() + verify(listener, never()).onUserSetupChanged() + verify(listener, never()).onDeviceProvisionedChanged() + verify(listener).onFrpActiveChanged() + } + + @Test fun testRemoveListener() { init() controller.addCallback(listener) @@ -220,11 +263,13 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { switchUser(10) settings.putIntForUser(Settings.Secure.USER_SETUP_COMPLETE, 1, START_USER) settings.putInt(Settings.Global.DEVICE_PROVISIONED, 1) + settings.putInt(Settings.Secure.SECURE_FRP_MODE, 1) testableLooper.processAllMessages() mainExecutor.runAllReady() verify(listener, never()).onDeviceProvisionedChanged() + verify(listener, never()).onFrpActiveChanged() verify(listener, never()).onUserSetupChanged() verify(listener, never()).onUserSwitched() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index d00acb89d228..3ed6cc88826c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -29,6 +29,8 @@ import android.os.UserManager import android.provider.Settings import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.R @@ -62,6 +64,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertNotNull import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -72,6 +75,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock @@ -96,6 +100,7 @@ class UserInteractorTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: UserInteractor @@ -154,6 +159,7 @@ class UserInteractorTest : SysuiTestCase() { repository = telephonyRepository, ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, @@ -177,6 +183,18 @@ class UserInteractorTest : SysuiTestCase() { } @Test + fun `testKeyguardUpdateMonitor_onKeyguardGoingAway`() = + testScope.runTest { + val argumentCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + verify(keyguardUpdateMonitor).registerCallback(argumentCaptor.capture()) + + argumentCaptor.value.onKeyguardGoingAway() + + val lastValue = collectLastValue(underTest.dialogDismissRequests) + assertNotNull(lastValue) + } + + @Test fun `onRecordSelected - user`() = testScope.runTest { val userInfos = createUserInfos(count = 3, includeGuest = false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt index 22fc32af1b80..daa71b942a2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt @@ -25,6 +25,7 @@ import android.graphics.drawable.BitmapDrawable import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase @@ -80,6 +81,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: StatusBarUserChipViewModel @@ -263,6 +265,7 @@ class StatusBarUserChipViewModelTest : SysuiTestCase() { repository = FakeTelephonyRepository(), ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index a2bd8d365192..e08ebf4a9050 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -23,6 +23,7 @@ import android.content.pm.UserInfo import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.GuestResetOrExitSessionReceiver import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase @@ -81,6 +82,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var underTest: UserSwitcherViewModel @@ -165,6 +167,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { repository = FakeTelephonyRepository(), ), broadcastDispatcher = fakeBroadcastDispatcher, + keyguardUpdateMonitor = keyguardUpdateMonitor, backgroundDispatcher = testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/service/ObservableServiceConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/service/ObservableServiceConnectionTest.java index 046ad1293521..f9bfafc13f35 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/service/ObservableServiceConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/service/ObservableServiceConnectionTest.java @@ -16,6 +16,8 @@ package com.android.systemui.util.service; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; @@ -169,4 +171,19 @@ public class ObservableServiceConnectionTest extends SysuiTestCase { verify(mCallback).onDisconnected(eq(connection), eq(ObservableServiceConnection.DISCONNECT_REASON_UNBIND)); } + + @Test + public void testBindServiceThrowsError() { + ObservableServiceConnection<Foo> connection = new ObservableServiceConnection<>(mContext, + mIntent, mExecutor, mTransformer); + connection.addCallback(mCallback); + + when(mContext.bindService(eq(mIntent), anyInt(), eq(mExecutor), eq(connection))) + .thenThrow(new SecurityException()); + + // Verify that the exception was caught and that bind returns false, and we properly + // unbind. + assertThat(connection.bind()).isFalse(); + verify(mContext).unbindService(connection); + } } diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 8b579ac6539d..2292d9b60713 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -1127,6 +1127,14 @@ class AutomaticBrightnessController { } } + public float convertToFloatScale(float nits) { + if (mCurrentBrightnessMapper != null) { + return mCurrentBrightnessMapper.convertToFloatScale(nits); + } else { + return PowerManager.BRIGHTNESS_INVALID_FLOAT; + } + } + public void recalculateSplines(boolean applyAdjustment, float[] adjustment) { mCurrentBrightnessMapper.recalculateSplines(applyAdjustment, adjustment); diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java index 3fc50c4edf6d..d0471837d79b 100644 --- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java +++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java @@ -322,6 +322,14 @@ public abstract class BrightnessMappingStrategy { public abstract float convertToNits(float brightness); /** + * Converts the provided nit value to a float scale value if possible. + * + * Returns {@link PowerManager.BRIGHTNESS_INVALID_FLOAT} if there's no available mapping for + * the nits to float scale. + */ + public abstract float convertToFloatScale(float nits); + + /** * Adds a user interaction data point to the brightness mapping. * * This data point <b>must</b> exist on the brightness curve as a result of this call. This is @@ -671,6 +679,11 @@ public abstract class BrightnessMappingStrategy { } @Override + public float convertToFloatScale(float nits) { + return PowerManager.BRIGHTNESS_INVALID_FLOAT; + } + + @Override public void addUserDataPoint(float lux, float brightness) { float unadjustedBrightness = getUnadjustedBrightness(lux); if (mLoggingEnabled) { @@ -913,6 +926,11 @@ public abstract class BrightnessMappingStrategy { } @Override + public float convertToFloatScale(float nits) { + return mNitsToBrightnessSpline.interpolate(nits); + } + + @Override public void addUserDataPoint(float lux, float brightness) { float unadjustedBrightness = getUnadjustedBrightness(lux); if (mLoggingEnabled) { diff --git a/services/core/java/com/android/server/display/BrightnessSetting.java b/services/core/java/com/android/server/display/BrightnessSetting.java index 74486113dcf9..9982d2eb31d1 100644 --- a/services/core/java/com/android/server/display/BrightnessSetting.java +++ b/services/core/java/com/android/server/display/BrightnessSetting.java @@ -117,6 +117,23 @@ public class BrightnessSetting { } } + /** + * @return The brightness for the default display in nits. Used when the underlying display + * device has changed but we want to persist the nit value. + */ + float getBrightnessNitsForDefaultDisplay() { + return mPersistentDataStore.getBrightnessNitsForDefaultDisplay(); + } + + /** + * Set brightness in nits for the default display. Used when we want to persist the nit value + * even if the underlying display device changes. + * @param nits The brightness value in nits + */ + void setBrightnessNitsForDefaultDisplay(float nits) { + mPersistentDataStore.setBrightnessNitsForDefaultDisplay(nits); + } + private void notifyListeners(float brightness) { for (BrightnessSettingListener l : mListeners) { l.onBrightnessChanged(brightness); diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 5ecc7f2b7bb8..25848002b936 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -233,6 +233,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // True if should use light sensor to automatically determine doze screen brightness. private final boolean mAllowAutoBrightnessWhileDozingConfig; + // True if we want to persist the brightness value in nits even if the underlying display + // device changes. + private final boolean mPersistBrightnessNitsForDefaultDisplay; + // True if the brightness config has changed and the short-term model needs to be reset private boolean mShouldResetShortTermModel; @@ -583,6 +587,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mAllowAutoBrightnessWhileDozingConfig = resources.getBoolean( com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing); + mPersistBrightnessNitsForDefaultDisplay = resources.getBoolean( + com.android.internal.R.bool.config_persistBrightnessNitsForDefaultDisplay); + mDisplayDeviceConfig = logicalDisplay.getPrimaryDisplayDeviceLocked() .getDisplayDeviceConfig(); @@ -651,7 +658,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call loadProximitySensor(); - mCurrentScreenBrightnessSetting = getScreenBrightnessSetting(); + loadNitBasedBrightnessSetting(); mScreenBrightnessForVr = getScreenBrightnessForVrSetting(); mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting(); mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; @@ -829,6 +836,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mDisplayStatsId = mUniqueDisplayId.hashCode(); mDisplayDeviceConfig = config; loadFromDisplayDeviceConfig(token, info, hbmMetadata); + loadNitBasedBrightnessSetting(); /// Since the underlying display-device changed, we really don't know the // last command that was sent to change it's state. Lets assume it is unknown so @@ -873,10 +881,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mAutomaticBrightnessController.stop(); } - if (mScreenOffBrightnessSensorController != null) { - mScreenOffBrightnessSensorController.stop(); - } - if (mBrightnessSetting != null) { mBrightnessSetting.unregisterListener(mBrightnessSettingListener); } @@ -1125,6 +1129,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mScreenOffBrightnessSensorController != null) { mScreenOffBrightnessSensorController.stop(); + mScreenOffBrightnessSensorController = null; } loadScreenOffBrightnessSensor(); int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux(); @@ -1242,6 +1247,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mPowerState.stop(); mPowerState = null; } + + if (mScreenOffBrightnessSensorController != null) { + mScreenOffBrightnessSensorController.stop(); + } } private void updatePowerState() { @@ -2497,10 +2506,33 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return clampScreenBrightnessForVr(brightnessFloat); } + private void loadNitBasedBrightnessSetting() { + if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) { + float brightnessNitsForDefaultDisplay = + mBrightnessSetting.getBrightnessNitsForDefaultDisplay(); + if (brightnessNitsForDefaultDisplay >= 0) { + float brightnessForDefaultDisplay = convertToFloatScale( + brightnessNitsForDefaultDisplay); + if (isValidBrightnessValue(brightnessForDefaultDisplay)) { + mBrightnessSetting.setBrightness(brightnessForDefaultDisplay); + mCurrentScreenBrightnessSetting = brightnessForDefaultDisplay; + return; + } + } + } + mCurrentScreenBrightnessSetting = getScreenBrightnessSetting(); + } + void setBrightness(float brightnessValue) { // Update the setting, which will eventually call back into DPC to have us actually update // the display with the new value. mBrightnessSetting.setBrightness(brightnessValue); + if (mDisplayId == Display.DEFAULT_DISPLAY && mPersistBrightnessNitsForDefaultDisplay) { + float nits = convertToNits(brightnessValue); + if (nits >= 0) { + mBrightnessSetting.setBrightnessNitsForDefaultDisplay(nits); + } + } } void onBootCompleted() { @@ -2513,7 +2545,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return; } setCurrentScreenBrightness(brightnessValue); - mBrightnessSetting.setBrightness(brightnessValue); + setBrightness(brightnessValue); } private void setCurrentScreenBrightness(float brightnessValue) { @@ -2592,6 +2624,13 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return mAutomaticBrightnessController.convertToNits(brightness); } + private float convertToFloatScale(float nits) { + if (mAutomaticBrightnessController == null) { + return PowerManager.BRIGHTNESS_INVALID_FLOAT; + } + return mAutomaticBrightnessController.convertToFloatScale(nits); + } + @GuardedBy("mLock") private void updatePendingProximityRequestsLocked() { mWaitingForNegativeProximity |= mPendingWaitForNegativeProximityLocked; @@ -2689,25 +2728,27 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call pw.println(" mScreenBrightnessForVrRangeMaximum=" + mScreenBrightnessForVrRangeMaximum); pw.println(" mScreenBrightnessForVrDefault=" + mScreenBrightnessForVrDefault); pw.println(" mUseSoftwareAutoBrightnessConfig=" + mUseSoftwareAutoBrightnessConfig); - pw.println(" mAllowAutoBrightnessWhileDozingConfig=" + - mAllowAutoBrightnessWhileDozingConfig); + pw.println(" mAllowAutoBrightnessWhileDozingConfig=" + + mAllowAutoBrightnessWhileDozingConfig); + pw.println(" mPersistBrightnessNitsForDefaultDisplay=" + + mPersistBrightnessNitsForDefaultDisplay); pw.println(" mSkipScreenOnBrightnessRamp=" + mSkipScreenOnBrightnessRamp); pw.println(" mColorFadeFadesConfig=" + mColorFadeFadesConfig); pw.println(" mColorFadeEnabled=" + mColorFadeEnabled); synchronized (mCachedBrightnessInfo) { - pw.println(" mCachedBrightnessInfo.brightness=" + - mCachedBrightnessInfo.brightness.value); - pw.println(" mCachedBrightnessInfo.adjustedBrightness=" + - mCachedBrightnessInfo.adjustedBrightness.value); - pw.println(" mCachedBrightnessInfo.brightnessMin=" + - mCachedBrightnessInfo.brightnessMin.value); - pw.println(" mCachedBrightnessInfo.brightnessMax=" + - mCachedBrightnessInfo.brightnessMax.value); + pw.println(" mCachedBrightnessInfo.brightness=" + + mCachedBrightnessInfo.brightness.value); + pw.println(" mCachedBrightnessInfo.adjustedBrightness=" + + mCachedBrightnessInfo.adjustedBrightness.value); + pw.println(" mCachedBrightnessInfo.brightnessMin=" + + mCachedBrightnessInfo.brightnessMin.value); + pw.println(" mCachedBrightnessInfo.brightnessMax=" + + mCachedBrightnessInfo.brightnessMax.value); pw.println(" mCachedBrightnessInfo.hbmMode=" + mCachedBrightnessInfo.hbmMode.value); - pw.println(" mCachedBrightnessInfo.hbmTransitionPoint=" + - mCachedBrightnessInfo.hbmTransitionPoint.value); - pw.println(" mCachedBrightnessInfo.brightnessMaxReason =" + - mCachedBrightnessInfo.brightnessMaxReason.value); + pw.println(" mCachedBrightnessInfo.hbmTransitionPoint=" + + mCachedBrightnessInfo.hbmTransitionPoint.value); + pw.println(" mCachedBrightnessInfo.brightnessMaxReason =" + + mCachedBrightnessInfo.brightnessMaxReason.value); } pw.println(" mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig); pw.println(" mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig); diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java index 73131a1dc220..a8e0d58cc4ff 100644 --- a/services/core/java/com/android/server/display/PersistentDataStore.java +++ b/services/core/java/com/android/server/display/PersistentDataStore.java @@ -94,6 +94,7 @@ import java.util.Objects; * </brightness-curve> * </brightness-configuration> * </brightness-configurations> + * <brightness-nits-for-default-display>600</brightness-nits-for-default-display> * </display-manager-state> * </code> * @@ -130,6 +131,9 @@ final class PersistentDataStore { private static final String TAG_RESOLUTION_HEIGHT = "resolution-height"; private static final String TAG_REFRESH_RATE = "refresh-rate"; + private static final String TAG_BRIGHTNESS_NITS_FOR_DEFAULT_DISPLAY = + "brightness-nits-for-default-display"; + // Remembered Wifi display devices. private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>(); @@ -137,6 +141,8 @@ final class PersistentDataStore { private final HashMap<String, DisplayState> mDisplayStates = new HashMap<String, DisplayState>(); + private float mBrightnessNitsForDefaultDisplay = -1; + // Display values which should be stable across the device's lifetime. private final StableDeviceValues mStableDeviceValues = new StableDeviceValues(); @@ -312,6 +318,19 @@ final class PersistentDataStore { return false; } + public float getBrightnessNitsForDefaultDisplay() { + return mBrightnessNitsForDefaultDisplay; + } + + public boolean setBrightnessNitsForDefaultDisplay(float nits) { + if (nits != mBrightnessNitsForDefaultDisplay) { + mBrightnessNitsForDefaultDisplay = nits; + setDirty(); + return true; + } + return false; + } + public boolean setUserPreferredRefreshRate(DisplayDevice displayDevice, float refreshRate) { final String displayDeviceUniqueId = displayDevice.getUniqueId(); if (!displayDevice.hasStableUniqueId() || displayDeviceUniqueId == null) { @@ -513,6 +532,10 @@ final class PersistentDataStore { if (parser.getName().equals(TAG_BRIGHTNESS_CONFIGURATIONS)) { mGlobalBrightnessConfigurations.loadFromXml(parser); } + if (parser.getName().equals(TAG_BRIGHTNESS_NITS_FOR_DEFAULT_DISPLAY)) { + String value = parser.nextText(); + mBrightnessNitsForDefaultDisplay = Float.parseFloat(value); + } } } @@ -592,6 +615,9 @@ final class PersistentDataStore { serializer.startTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); mGlobalBrightnessConfigurations.saveToXml(serializer); serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATIONS); + serializer.startTag(null, TAG_BRIGHTNESS_NITS_FOR_DEFAULT_DISPLAY); + serializer.text(Float.toString(mBrightnessNitsForDefaultDisplay)); + serializer.endTag(null, TAG_BRIGHTNESS_NITS_FOR_DEFAULT_DISPLAY); serializer.endTag(null, TAG_DISPLAY_MANAGER_STATE); serializer.endDocument(); } @@ -615,6 +641,7 @@ final class PersistentDataStore { mStableDeviceValues.dump(pw, " "); pw.println(" GlobalBrightnessConfigurations:"); mGlobalBrightnessConfigurations.dump(pw, " "); + pw.println(" mBrightnessNitsForDefaultDisplay=" + mBrightnessNitsForDefaultDisplay); } private static final class DisplayState { diff --git a/services/core/java/com/android/server/media/projection/OWNERS b/services/core/java/com/android/server/media/projection/OWNERS index 9ca391013aa3..832bcd9d70e6 100644 --- a/services/core/java/com/android/server/media/projection/OWNERS +++ b/services/core/java/com/android/server/media/projection/OWNERS @@ -1,2 +1 @@ -michaelwr@google.com -santoscordon@google.com +include /media/java/android/media/projection/OWNERS diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index ff10cbc19d5f..d11413937f8d 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -17,6 +17,7 @@ package com.android.server.notification; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; +import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_FOREGROUND_SERVICE; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY; @@ -6323,21 +6324,40 @@ public class NotificationManagerService extends SystemService { checkCallerIsSystem(); mHandler.post(() -> { synchronized (mNotificationLock) { - // strip flag from all enqueued notifications. listeners will be informed - // in post runnable. - List<NotificationRecord> enqueued = findNotificationsByListLocked( - mEnqueuedNotifications, pkg, null, notificationId, userId); - for (int i = 0; i < enqueued.size(); i++) { - removeForegroundServiceFlagLocked(enqueued.get(i)); - } + int count = getNotificationCount(pkg, userId); + boolean removeFgsNotification = false; + if (count > MAX_PACKAGE_NOTIFICATIONS) { + mUsageStats.registerOverCountQuota(pkg); + removeFgsNotification = true; + } + if (removeFgsNotification) { + NotificationRecord r = findNotificationLocked(pkg, null, notificationId, + userId); + if (r != null) { + if (DBG) { + Slog.d(TAG, "Remove FGS flag not allow. Cancel FGS notification"); + } + removeFromNotificationListsLocked(r); + cancelNotificationLocked(r, false, REASON_APP_CANCEL, true, + null, SystemClock.elapsedRealtime()); + } + } else { + // strip flag from all enqueued notifications. listeners will be informed + // in post runnable. + List<NotificationRecord> enqueued = findNotificationsByListLocked( + mEnqueuedNotifications, pkg, null, notificationId, userId); + for (int i = 0; i < enqueued.size(); i++) { + removeForegroundServiceFlagLocked(enqueued.get(i)); + } - // if posted notification exists, strip its flag and tell listeners - NotificationRecord r = findNotificationByListLocked( - mNotificationList, pkg, null, notificationId, userId); - if (r != null) { - removeForegroundServiceFlagLocked(r); - mRankingHelper.sort(mNotificationList); - mListeners.notifyPostedLocked(r, r); + // if posted notification exists, strip its flag and tell listeners + NotificationRecord r = findNotificationByListLocked( + mNotificationList, pkg, null, notificationId, userId); + if (r != null) { + removeForegroundServiceFlagLocked(r); + mRankingHelper.sort(mNotificationList); + mListeners.notifyPostedLocked(r, r); + } } } }); @@ -6483,9 +6503,17 @@ public class NotificationManagerService extends SystemService { checkRestrictedCategories(notification); + // Notifications passed to setForegroundService() have FLAG_FOREGROUND_SERVICE, + // but it's also possible that the app has called notify() with an update to an + // FGS notification that hasn't yet been displayed. Make sure we check for any + // FGS-related situation up front, outside of any locks so it's safe to call into + // the Activity Manager. + final ServiceNotificationPolicy policy = mAmi.applyForegroundServiceNotification( + notification, tag, id, pkg, userId); + // Fix the notification as best we can. try { - fixNotification(notification, pkg, tag, id, userId); + fixNotification(notification, pkg, tag, id, userId, notificationUid, policy); } catch (Exception e) { if (notification.isForegroundService()) { throw new SecurityException("Invalid FGS notification", e); @@ -6494,13 +6522,7 @@ public class NotificationManagerService extends SystemService { return; } - // Notifications passed to setForegroundService() have FLAG_FOREGROUND_SERVICE, - // but it's also possible that the app has called notify() with an update to an - // FGS notification that hasn't yet been displayed. Make sure we check for any - // FGS-related situation up front, outside of any locks so it's safe to call into - // the Activity Manager. - final ServiceNotificationPolicy policy = mAmi.applyForegroundServiceNotification( - notification, tag, id, pkg, userId); + if (policy == ServiceNotificationPolicy.UPDATE_ONLY) { // Proceed if the notification is already showing/known, otherwise ignore // because the service lifecycle logic has retained responsibility for its @@ -6663,14 +6685,20 @@ public class NotificationManagerService extends SystemService { @VisibleForTesting protected void fixNotification(Notification notification, String pkg, String tag, int id, - int userId) throws NameNotFoundException, RemoteException { + @UserIdInt int userId, int notificationUid, ServiceNotificationPolicy fgsPolicy) + throws NameNotFoundException, RemoteException { final ApplicationInfo ai = mPackageManagerClient.getApplicationInfoAsUser( pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, (userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId); Notification.addFieldsFromContext(ai, notification); - int canColorize = mPackageManagerClient.checkPermission( - android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, pkg); + if (notification.isForegroundService() && fgsPolicy == NOT_FOREGROUND_SERVICE) { + notification.flags &= ~FLAG_FOREGROUND_SERVICE; + } + + int canColorize = getContext().checkPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, -1, notificationUid); + if (canColorize == PERMISSION_GRANTED) { notification.flags |= Notification.FLAG_CAN_COLORIZE; } else { @@ -7059,6 +7087,29 @@ public class NotificationManagerService extends SystemService { return mPermissionHelper.hasPermission(uid); } + private int getNotificationCount(String pkg, int userId) { + int count = 0; + synchronized (mNotificationLock) { + final int numListSize = mNotificationList.size(); + for (int i = 0; i < numListSize; i++) { + final NotificationRecord existing = mNotificationList.get(i); + if (existing.getSbn().getPackageName().equals(pkg) + && existing.getSbn().getUserId() == userId) { + count++; + } + } + final int numEnqSize = mEnqueuedNotifications.size(); + for (int i = 0; i < numEnqSize; i++) { + final NotificationRecord existing = mEnqueuedNotifications.get(i); + if (existing.getSbn().getPackageName().equals(pkg) + && existing.getSbn().getUserId() == userId) { + count++; + } + } + } + return count; + } + protected int getNotificationCount(String pkg, int userId, int excludedId, String excludedTag) { int count = 0; diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 7fc46fd6f757..a2b2983a8f35 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -1641,7 +1641,7 @@ public class ShortcutService extends IShortcutService.Stub { return false; } int uid = injectGetPackageUid(systemChooser.getPackageName(), UserHandle.USER_SYSTEM); - return uid == callingUid; + return UserHandle.getAppId(uid) == UserHandle.getAppId(callingUid); } private void enforceSystemOrShell() { diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index f913cef99813..bb6db9aa4993 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -199,6 +199,7 @@ import com.android.internal.policy.PhoneWindow; import com.android.internal.policy.TransitionAnimation; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.ArrayUtils; +import com.android.internal.widget.LockPatternUtils; import com.android.server.ExtconStateObserver; import com.android.server.ExtconUEventObserver; import com.android.server.GestureLauncherService; @@ -407,6 +408,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { AppOpsManager mAppOpsManager; PackageManager mPackageManager; SideFpsEventHandler mSideFpsEventHandler; + LockPatternUtils mLockPatternUtils; private boolean mHasFeatureAuto; private boolean mHasFeatureWatch; private boolean mHasFeatureLeanback; @@ -1056,8 +1058,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { } synchronized (mLock) { - // Lock the device after the dream transition has finished. - mLockAfterAppTransitionFinished = true; + // If the setting to lock instantly on power button press is true, then set the flag to + // lock after the dream transition has finished. + mLockAfterAppTransitionFinished = + mLockPatternUtils.getPowerButtonInstantlyLocks(mCurrentUserId); } dreamManagerInternal.requestDream(); @@ -1929,6 +1933,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mHasFeatureHdmiCec = mPackageManager.hasSystemFeature(FEATURE_HDMI_CEC); mAccessibilityShortcutController = new AccessibilityShortcutController(mContext, new Handler(), mCurrentUserId); + mLockPatternUtils = new LockPatternUtils(mContext); mLogger = new MetricsLogger(); mScreenOffSleepTokenAcquirer = mActivityTaskManagerInternal diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index b4b8cf9a9eab..43fb44ca02af 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -169,6 +169,7 @@ import android.view.Display; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.procstats.IProcessStats; import com.android.internal.app.procstats.ProcessStats; +import com.android.internal.app.procstats.StatsEventOutput; import com.android.internal.os.BackgroundThread; import com.android.internal.os.BinderCallsStats.ExportedCallStat; import com.android.internal.os.KernelAllocationStats; @@ -612,12 +613,19 @@ public class StatsPullAtomService extends SystemService { } case FrameworkStatsLog.PROC_STATS: synchronized (mProcStatsLock) { - return pullProcStatsLocked(ProcessStats.REPORT_ALL, atomTag, data); + return pullProcStatsLocked(atomTag, data); } case FrameworkStatsLog.PROC_STATS_PKG_PROC: synchronized (mProcStatsLock) { - return pullProcStatsLocked(ProcessStats.REPORT_PKG_PROC_STATS, atomTag, - data); + return pullProcStatsLocked(atomTag, data); + } + case FrameworkStatsLog.PROCESS_STATE: + synchronized (mProcStatsLock) { + return pullProcessStateLocked(atomTag, data); + } + case FrameworkStatsLog.PROCESS_ASSOCIATION: + synchronized (mProcStatsLock) { + return pullProcessAssociationLocked(atomTag, data); } case FrameworkStatsLog.DISK_IO: synchronized (mDiskIoLock) { @@ -890,6 +898,8 @@ public class StatsPullAtomService extends SystemService { registerNumFacesEnrolled(); registerProcStats(); registerProcStatsPkgProc(); + registerProcessState(); + registerProcessAssociation(); registerDiskIO(); registerPowerProfile(); registerProcessCpuTime(); @@ -2870,59 +2880,138 @@ public class StatsPullAtomService extends SystemService { ); } - private int pullProcStatsLocked(int section, int atomTag, List<StatsEvent> pulledData) { + private void registerProcessState() { + int tagId = FrameworkStatsLog.PROCESS_STATE; + mStatsManager.setPullAtomCallback( + tagId, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, + mStatsCallbackImpl); + } + + private void registerProcessAssociation() { + int tagId = FrameworkStatsLog.PROCESS_ASSOCIATION; + mStatsManager.setPullAtomCallback( + tagId, + null, // use default PullAtomMetadata values + DIRECT_EXECUTOR, + mStatsCallbackImpl); + } + + @GuardedBy("mProcStatsLock") + private ProcessStats getStatsFromProcessStatsService(int atomTag) { IProcessStats processStatsService = getIProcessStatsService(); if (processStatsService == null) { - return StatsManager.PULL_SKIP; + return null; } - final long token = Binder.clearCallingIdentity(); try { // force procstats to flush & combine old files into one store - long lastHighWaterMark = readProcStatsHighWaterMark(section); - - ProtoOutputStream[] protoStreams = new ProtoOutputStream[MAX_PROCSTATS_SHARDS]; - for (int i = 0; i < protoStreams.length; i++) { - protoStreams[i] = new ProtoOutputStream(); - } - + long lastHighWaterMark = readProcStatsHighWaterMark(atomTag); ProcessStats procStats = new ProcessStats(false); // Force processStatsService to aggregate all in-storage and in-memory data. - long highWaterMark = processStatsService.getCommittedStatsMerged( - lastHighWaterMark, section, true, null, procStats); - procStats.dumpAggregatedProtoForStatsd(protoStreams, MAX_PROCSTATS_RAW_SHARD_SIZE); - - for (int i = 0; i < protoStreams.length; i++) { - byte[] bytes = protoStreams[i].getBytes(); // cache the value - if (bytes.length > 0) { - pulledData.add(FrameworkStatsLog.buildStatsEvent(atomTag, bytes, - // This is a shard ID, and is specified in the metric definition to be - // a dimension. This will result in statsd using RANDOM_ONE_SAMPLE to - // keep all the shards, as it thinks each shard is a different dimension - // of data. - i)); - } - } - - new File(mBaseDir.getAbsolutePath() + "/" + section + "_" + lastHighWaterMark) + long highWaterMark = + processStatsService.getCommittedStatsMerged( + lastHighWaterMark, + ProcessStats.REPORT_ALL, // ignored since committedStats below is null. + true, + null, // committedStats + procStats); + new File( + mBaseDir.getAbsolutePath() + + "/" + + highWaterMarkFilePrefix(atomTag) + + "_" + + lastHighWaterMark) .delete(); - new File(mBaseDir.getAbsolutePath() + "/" + section + "_" + highWaterMark) + new File( + mBaseDir.getAbsolutePath() + + "/" + + highWaterMarkFilePrefix(atomTag) + + "_" + + highWaterMark) .createNewFile(); + return procStats; } catch (RemoteException | IOException e) { Slog.e(TAG, "Getting procstats failed: ", e); - return StatsManager.PULL_SKIP; + return null; } finally { Binder.restoreCallingIdentity(token); } + } + + @GuardedBy("mProcStatsLock") + private int pullProcStatsLocked(int atomTag, List<StatsEvent> pulledData) { + ProcessStats procStats = getStatsFromProcessStatsService(atomTag); + if (procStats == null) { + return StatsManager.PULL_SKIP; + } + ProtoOutputStream[] protoStreams = new ProtoOutputStream[MAX_PROCSTATS_SHARDS]; + for (int i = 0; i < protoStreams.length; i++) { + protoStreams[i] = new ProtoOutputStream(); + } + procStats.dumpAggregatedProtoForStatsd(protoStreams, MAX_PROCSTATS_RAW_SHARD_SIZE); + for (int i = 0; i < protoStreams.length; i++) { + byte[] bytes = protoStreams[i].getBytes(); // cache the value + if (bytes.length > 0) { + pulledData.add( + FrameworkStatsLog.buildStatsEvent( + atomTag, + bytes, + // This is a shard ID, and is specified in the metric definition to + // be + // a dimension. This will result in statsd using RANDOM_ONE_SAMPLE + // to + // keep all the shards, as it thinks each shard is a different + // dimension + // of data. + i)); + } + } + return StatsManager.PULL_SUCCESS; + } + + @GuardedBy("mProcStatsLock") + private int pullProcessStateLocked(int atomTag, List<StatsEvent> pulledData) { + ProcessStats procStats = getStatsFromProcessStatsService(atomTag); + if (procStats == null) { + return StatsManager.PULL_SKIP; + } + procStats.dumpProcessState(atomTag, new StatsEventOutput(pulledData)); + return StatsManager.PULL_SUCCESS; + } + + @GuardedBy("mProcStatsLock") + private int pullProcessAssociationLocked(int atomTag, List<StatsEvent> pulledData) { + ProcessStats procStats = getStatsFromProcessStatsService(atomTag); + if (procStats == null) { + return StatsManager.PULL_SKIP; + } + procStats.dumpProcessAssociation(atomTag, new StatsEventOutput(pulledData)); return StatsManager.PULL_SUCCESS; } + private String highWaterMarkFilePrefix(int atomTag) { + // For backward compatibility, use the legacy ProcessStats enum value as the prefix for + // PROC_STATS and PROC_STATS_PKG_PROC. + if (atomTag == FrameworkStatsLog.PROC_STATS) { + return String.valueOf(ProcessStats.REPORT_ALL); + } + if (atomTag == FrameworkStatsLog.PROC_STATS_PKG_PROC) { + return String.valueOf(ProcessStats.REPORT_PKG_PROC_STATS); + } + return "atom-" + atomTag; + } + // read high watermark for section - private long readProcStatsHighWaterMark(int section) { + private long readProcStatsHighWaterMark(int atomTag) { try { - File[] files = mBaseDir.listFiles((d, name) -> { - return name.toLowerCase().startsWith(String.valueOf(section) + '_'); - }); + File[] files = + mBaseDir.listFiles( + (d, name) -> { + return name.toLowerCase() + .startsWith(highWaterMarkFilePrefix(atomTag) + '_'); + }); if (files == null || files.length == 0) { return 0; } diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 7489f80946eb..7c9244e39e67 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -696,6 +696,8 @@ class ActivityClientController extends IActivityClientController.Stub { synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); if (r != null) { + EventLogTags.writeWmSetRequestedOrientation(requestedOrientation, + r.shortComponentName); r.setRequestedOrientation(requestedOrientation); } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 61f320337e3a..681d5b6e34fb 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1487,6 +1487,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLastReportedMultiWindowMode = inPictureInPictureMode; ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS, true /* ignoreVisibility */); + if (inPictureInPictureMode && findMainWindow() == null) { + // Prevent malicious app entering PiP without valid WindowState, which can in turn + // result a non-touchable PiP window since the InputConsumer for PiP requires it. + EventLog.writeEvent(0x534e4554, "265293293", -1, ""); + removeImmediately(); + } } } @@ -7727,27 +7733,38 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** * Returns the requested {@link Configuration.Orientation} for the current activity. + */ + @Configuration.Orientation + @Override + int getRequestedConfigurationOrientation(boolean forDisplay) { + return getRequestedConfigurationOrientation(forDisplay, getOverrideOrientation()); + } + + /** + * Returns the requested {@link Configuration.Orientation} for the requested + * {@link ActivityInfo.ScreenOrientation}. * - * <p>When The current orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns the - * requested orientation for the activity below which is the first activity with an explicit + * <p>When the current screen orientation is set to {@link SCREEN_ORIENTATION_BEHIND} it returns + * the requested orientation for the activity below which is the first activity with an explicit * (different from {@link SCREEN_ORIENTATION_UNSET}) orientation which is not {@link * SCREEN_ORIENTATION_BEHIND}. */ @Configuration.Orientation - @Override - int getRequestedConfigurationOrientation(boolean forDisplay) { + int getRequestedConfigurationOrientation(boolean forDisplay, + @ActivityInfo.ScreenOrientation int requestedOrientation) { if (mLetterboxUiController.hasInheritedOrientation()) { final RootDisplayArea root = getRootDisplayArea(); if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) { - return ActivityInfo.reverseOrientation( + return reverseConfigurationOrientation( mLetterboxUiController.getInheritedOrientation()); } else { return mLetterboxUiController.getInheritedOrientation(); } } - if (task != null && getOverrideOrientation() == SCREEN_ORIENTATION_BEHIND) { + if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) { // We use Task here because we want to be consistent with what happens in // multi-window mode where other tasks orientations are ignored. + android.util.Log.d("orientation", "We are here"); final ActivityRecord belowCandidate = task.getActivity( a -> a.canDefineOrientationForActivitiesAbove() /* callback */, this /* boundary */, false /* includeBoundary */, @@ -7756,7 +7773,23 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return belowCandidate.getRequestedConfigurationOrientation(forDisplay); } } - return super.getRequestedConfigurationOrientation(forDisplay); + return super.getRequestedConfigurationOrientation(forDisplay, requestedOrientation); + } + + /** + * Returns the reversed configuration orientation. + * @hide + */ + @Configuration.Orientation + public static int reverseConfigurationOrientation(@Configuration.Orientation int orientation) { + switch (orientation) { + case ORIENTATION_LANDSCAPE: + return ORIENTATION_PORTRAIT; + case ORIENTATION_PORTRAIT: + return ORIENTATION_LANDSCAPE; + default: + return orientation; + } } /** @@ -7802,6 +7835,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) { return; } + // This is necessary in order to avoid going into size compat mode when the orientation + // change request comes from the app + if (mWmService.mLetterboxConfiguration + .isSizeCompatModeDisabledAfterOrientationChangeFromApp() + && getRequestedConfigurationOrientation(false, requestedOrientation) + != getRequestedConfigurationOrientation(false /*forDisplay */)) { + // Do not change the requested configuration now, because this will be done when setting + // the orientation below with the new mCompatDisplayInsets + clearSizeCompatModeAttributes(); + } + ProtoLog.v(WM_DEBUG_ORIENTATION, + "Setting requested orientation %s for %s", + ActivityInfo.screenOrientationToString(requestedOrientation), this); setOrientation(requestedOrientation, this); // Push the new configuration to the requested app in case where it's not pushed, e.g. when @@ -8021,17 +8067,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mDisplayContent, this, mLetterboxBoundsForFixedOrientationAndAspectRatio); } - @VisibleForTesting - void clearSizeCompatMode() { - final float lastSizeCompatScale = mSizeCompatScale; + private void clearSizeCompatModeAttributes() { mInSizeCompatModeForBounds = false; mSizeCompatScale = 1f; mSizeCompatBounds = null; mCompatDisplayInsets = null; + } + + @VisibleForTesting + void clearSizeCompatMode() { + final float lastSizeCompatScale = mSizeCompatScale; + clearSizeCompatModeAttributes(); if (mSizeCompatScale != lastSizeCompatScale) { forAllWindows(WindowState::updateGlobalScale, false /* traverseTopToBottom */); } - // Clear config override in #updateCompatDisplayInsets(). final int activityType = getActivityType(); final Configuration overrideConfig = getRequestedOverrideConfiguration(); @@ -8099,9 +8148,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (isFixedOrientationLetterboxAllowed) { resolveFixedOrientationConfiguration(newParentConfiguration); } - - if (getCompatDisplayInsets() != null) { - resolveSizeCompatModeConfiguration(newParentConfiguration); + final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets(); + if (compatDisplayInsets != null) { + resolveSizeCompatModeConfiguration(newParentConfiguration, compatDisplayInsets); } else if (inMultiWindowMode() && !isFixedOrientationLetterboxAllowed) { // We ignore activities' requested orientation in multi-window modes. They may be // taken into consideration in resolveFixedOrientationConfiguration call above. @@ -8118,7 +8167,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A resolveAspectRatioRestriction(newParentConfiguration); } - if (isFixedOrientationLetterboxAllowed || getCompatDisplayInsets() != null + if (isFixedOrientationLetterboxAllowed || compatDisplayInsets != null // In fullscreen, can be letterboxed for aspect ratio. || !inMultiWindowMode()) { updateResolvedBoundsPosition(newParentConfiguration); @@ -8126,7 +8175,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean isIgnoreOrientationRequest = mDisplayContent != null && mDisplayContent.getIgnoreOrientationRequest(); - if (getCompatDisplayInsets() == null + if (compatDisplayInsets == null // for size compat mode set in updateCompatDisplayInsets // Fixed orientation letterboxing is possible on both large screen devices // with ignoreOrientationRequest enabled and on phones in split screen even with @@ -8173,7 +8222,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A info.neverSandboxDisplayApis(sConstrainDisplayApisConfig), info.alwaysSandboxDisplayApis(sConstrainDisplayApisConfig), !matchParentBounds(), - getCompatDisplayInsets() != null, + compatDisplayInsets != null, shouldCreateCompatDisplayInsets()); } resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); @@ -8581,7 +8630,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * Resolves consistent screen configuration for orientation and rotation changes without * inheriting the parent bounds. */ - private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration) { + private void resolveSizeCompatModeConfiguration(Configuration newParentConfiguration, + @NonNull CompatDisplayInsets compatDisplayInsets) { final Configuration resolvedConfig = getResolvedOverrideConfiguration(); final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); @@ -8602,13 +8652,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ? requestedOrientation // We should use the original orientation of the activity when possible to avoid // forcing the activity in the opposite orientation. - : getCompatDisplayInsets().mOriginalRequestedOrientation != ORIENTATION_UNDEFINED - ? getCompatDisplayInsets().mOriginalRequestedOrientation + : compatDisplayInsets.mOriginalRequestedOrientation != ORIENTATION_UNDEFINED + ? compatDisplayInsets.mOriginalRequestedOrientation : newParentConfiguration.orientation; int rotation = newParentConfiguration.windowConfiguration.getRotation(); final boolean isFixedToUserRotation = mDisplayContent == null || mDisplayContent.getDisplayRotation().isFixedToUserRotation(); - if (!isFixedToUserRotation && !getCompatDisplayInsets().mIsFloating) { + if (!isFixedToUserRotation && !compatDisplayInsets.mIsFloating) { // Use parent rotation because the original display can be rotated. resolvedConfig.windowConfiguration.setRotation(rotation); } else { @@ -8624,11 +8674,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // rely on them to contain the original and unchanging width and height of the app. final Rect containingAppBounds = new Rect(); final Rect containingBounds = mTmpBounds; - getCompatDisplayInsets().getContainerBounds(containingAppBounds, containingBounds, rotation, + compatDisplayInsets.getContainerBounds(containingAppBounds, containingBounds, rotation, orientation, orientationRequested, isFixedToUserRotation); resolvedBounds.set(containingBounds); // The size of floating task is fixed (only swap), so the aspect ratio is already correct. - if (!getCompatDisplayInsets().mIsFloating) { + if (!compatDisplayInsets.mIsFloating) { mIsAspectRatioApplied = applyAspectRatio(resolvedBounds, containingAppBounds, containingBounds); } @@ -8637,7 +8687,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // are calculated in compat container space. The actual position on screen will be applied // later, so the calculation is simpler that doesn't need to involve offset from parent. getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration, - getCompatDisplayInsets()); + compatDisplayInsets); // Use current screen layout as source because the size of app is independent to parent. resolvedConfig.screenLayout = TaskFragment.computeScreenLayoutOverride( getConfiguration().screenLayout, resolvedConfig.screenWidthDp, diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 491e58b63f76..c527310abb14 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1486,7 +1486,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { a.persistableMode = ActivityInfo.PERSIST_NEVER; a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT; - a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_NO_HISTORY; + a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS; a.resizeMode = RESIZE_MODE_UNRESIZEABLE; a.configChanges = 0xffffffff; diff --git a/services/core/java/com/android/server/wm/EventLogTags.logtags b/services/core/java/com/android/server/wm/EventLogTags.logtags index 1e5a219e5e52..48ce22e227dd 100644 --- a/services/core/java/com/android/server/wm/EventLogTags.logtags +++ b/services/core/java/com/android/server/wm/EventLogTags.logtags @@ -1,4 +1,4 @@ -# See system/core/logcat/event.logtags for a description of the format of this file. +# See system/logging/logcat/event.logtags for a description of the format of this file. option java_package com.android.server.wm @@ -62,6 +62,10 @@ option java_package com.android.server.wm 31002 wm_task_moved (TaskId|1|5),(ToTop|1),(Index|1) # Task removed with source explanation. 31003 wm_task_removed (TaskId|1|5),(Reason|3) + +# Set the requested orientation of an activity. +31006 wm_set_requested_orientation (Orientation|1|5),(Component Name|3) + # bootanim finished: 31007 wm_boot_animation_done (time|2|3) diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index fa49a6ba6c2b..37cf5bc95a23 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -19,6 +19,7 @@ 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 static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ALLOW_IGNORE_ORIENTATION_REQUEST; +import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP; import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_CAMERA_COMPAT_TREATMENT; import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_COMPAT_FAKE_FOCUS; import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY; @@ -314,6 +315,10 @@ final class LetterboxConfiguration { mDeviceConfig.updateFlagActiveStatus( /* isActive */ mTranslucentLetterboxingEnabled, /* key */ KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY); + mDeviceConfig.updateFlagActiveStatus( + /* isActive */ true, + /* key */ KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP); + mLetterboxConfigurationPersister = letterboxConfigurationPersister; mLetterboxConfigurationPersister.start(); } @@ -327,6 +332,16 @@ final class LetterboxConfiguration { } /** + * Whether size compat mode is disabled after an orientation change request comes from the app. + * This value is controlled via {@link android.provider.DeviceConfig}. + */ + // TODO(b/270356567) Clean up this flag + boolean isSizeCompatModeDisabledAfterOrientationChangeFromApp() { + return mDeviceConfig.getFlag( + KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP); + } + + /** * Overrides the aspect ratio of letterbox for fixed orientation. If given value is <= {@link * #MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO}, both it and a value of {@link * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio} will be ignored and diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java index df3c8f0fdccc..1651af328b1f 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java +++ b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.provider.DeviceConfig; import android.util.ArraySet; - import com.android.internal.annotations.VisibleForTesting; import java.util.Map; @@ -53,6 +52,11 @@ final class LetterboxConfigurationDeviceConfig private static final boolean DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY = true; + static final String KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP = + "disable_size_compat_mode_after_orientation_change_from_app"; + private static final boolean + DEFAULT_VALUE_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP = true; + @VisibleForTesting static final Map<String, Boolean> sKeyToDefaultValueMap = Map.of( KEY_ENABLE_CAMERA_COMPAT_TREATMENT, @@ -64,7 +68,9 @@ final class LetterboxConfigurationDeviceConfig KEY_ENABLE_COMPAT_FAKE_FOCUS, DEFAULT_VALUE_ENABLE_COMPAT_FAKE_FOCUS, KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY, - DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY + DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY, + KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP, + DEFAULT_VALUE_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP ); // Whether camera compatibility treatment is enabled. @@ -93,6 +99,10 @@ final class LetterboxConfigurationDeviceConfig private boolean mIsTranslucentLetterboxingAllowed = DEFAULT_VALUE_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY; + // Whether size compat mode is disabled after an orientation change request comes from the app + private boolean mIsSizeCompatModeDisabledAfterOrientationChangeFromApp = + DEFAULT_VALUE_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP; + // Set of active device configs that need to be updated in // DeviceConfig.OnPropertiesChangedListener#onPropertiesChanged. private final ArraySet<String> mActiveDeviceConfigsSet = new ArraySet<>(); @@ -142,6 +152,8 @@ final class LetterboxConfigurationDeviceConfig return mIsCompatFakeFocusAllowed; case KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY: return mIsTranslucentLetterboxingAllowed; + case KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP: + return mIsSizeCompatModeDisabledAfterOrientationChangeFromApp; default: throw new AssertionError("Unexpected flag name: " + key); } @@ -169,6 +181,10 @@ final class LetterboxConfigurationDeviceConfig case KEY_ENABLE_LETTERBOX_TRANSLUCENT_ACTIVITY: mIsTranslucentLetterboxingAllowed = getDeviceConfig(key, defaultValue); break; + case KEY_DISABLE_SIZE_COMPAT_MODE_AFTER_ORIENTATION_CHANGE_FROM_APP: + mIsSizeCompatModeDisabledAfterOrientationChangeFromApp = + getDeviceConfig(key, defaultValue); + break; default: throw new AssertionError("Unexpected flag name: " + key); } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index c20a51338739..d9f2b6e4a0a3 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -1433,7 +1433,7 @@ final class LetterboxUiController { * the first opaque activity beneath. */ boolean hasInheritedLetterboxBehavior() { - return mLetterboxConfigListener != null && !mActivityRecord.matchParentBounds(); + return mLetterboxConfigListener != null; } /** diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 9e0e8c477573..7ff92afc9d29 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1456,7 +1456,26 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< */ @Configuration.Orientation int getRequestedConfigurationOrientation(boolean forDisplay) { - int requestedOrientation = getOverrideOrientation(); + return getRequestedConfigurationOrientation(forDisplay, getOverrideOrientation()); + } + + /** + * Gets the configuration orientation by the requested screen orientation + * + * @param forDisplay whether it is the requested config orientation for display. + * If {@code true}, we may reverse the requested orientation if the root is + * different from the display, so that when the display rotates to the + * reversed orientation, the requested app will be in the requested + * orientation. + * @param requestedOrientation the screen orientation({@link ScreenOrientation}) that is + * requested + * @return orientation in ({@link Configuration#ORIENTATION_LANDSCAPE}, + * {@link Configuration#ORIENTATION_PORTRAIT}, + * {@link Configuration#ORIENTATION_UNDEFINED}). + */ + @Configuration.Orientation + int getRequestedConfigurationOrientation(boolean forDisplay, + @ScreenOrientation int requestedOrientation) { final RootDisplayArea root = getRootDisplayArea(); if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) { // Reverse the requested orientation if the orientation of its root is different from @@ -1466,7 +1485,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< // (portrait). // When an app below the DAG is requesting landscape, it should actually request the // display to be portrait, so that the DAG and the app will be in landscape. - requestedOrientation = reverseOrientation(getOverrideOrientation()); + requestedOrientation = reverseOrientation(requestedOrientation); } if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 8931d8030df2..e6c1e75da581 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -6198,9 +6198,10 @@ public class WindowManagerService extends IWindowManager.Stub waitingForConfig = waitingForRemoteDisplayChange = false; numOpeningApps = 0; } - if (waitingForConfig || waitingForRemoteDisplayChange || mAppsFreezingScreen > 0 - || mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_ACTIVE - || mClientFreezingScreen || numOpeningApps > 0) { + final boolean waitingForApps = mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_TIMEOUT + && (mAppsFreezingScreen > 0 || numOpeningApps > 0); + if (waitingForConfig || waitingForRemoteDisplayChange || waitingForApps + || mClientFreezingScreen) { ProtoLog.d(WM_DEBUG_ORIENTATION, "stopFreezingDisplayLocked: Returning " + "waitingForConfig=%b, waitingForRemoteDisplayChange=%b, " + "mAppsFreezingScreen=%d, mWindowsFreezingScreen=%d, " diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 52f2b6351265..41e0fd715889 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -4577,7 +4577,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP void requestUpdateWallpaperIfNeeded() { final DisplayContent dc = getDisplayContent(); - if (dc != null && hasWallpaper()) { + if (dc != null && ((mIsWallpaper && !mLastConfigReportedToClient) || hasWallpaper())) { dc.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER; dc.setLayoutNeeded(); mWmService.mWindowPlacerLocked.requestTraversal(); diff --git a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java index 0993295e162f..2505abf2d160 100644 --- a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java @@ -152,6 +152,8 @@ class ContactsQueryHelper { } } catch (SQLiteException exception) { Slog.w("SQLite exception when querying contacts.", exception); + } catch (IllegalArgumentException exception) { + Slog.w("Illegal Argument exception when querying contacts.", exception); } if (found && lookupKey != null && hasPhoneNumber) { return queryPhoneNumber(lookupKey); diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index eff9e8da9a76..872734f7a01d 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -129,7 +129,6 @@ public class DataManager { private final List<PeopleService.ConversationsListener> mConversationsListeners = new ArrayList<>(1); private final Handler mHandler; - private ContentObserver mCallLogContentObserver; private ContentObserver mMmsSmsContentObserver; @@ -1106,6 +1105,7 @@ public class DataManager { @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) { mInjector.getBackgroundExecutor().execute(() -> { PackageData packageData = getPackage(packageName, user.getIdentifier()); + boolean hasCachedShortcut = false; for (ShortcutInfo shortcut : shortcuts) { if (ShortcutHelper.isConversationShortcut( shortcut, mShortcutServiceInternal, user.getIdentifier())) { @@ -1114,15 +1114,18 @@ public class DataManager { ? packageData.getConversationInfo(shortcut.getId()) : null; if (conversationInfo == null || !conversationInfo.isShortcutCachedForNotification()) { - // This is a newly cached shortcut. Clean up the existing cached - // shortcuts to ensure the cache size is under the limit. - cleanupCachedShortcuts(user.getIdentifier(), - MAX_CACHED_RECENT_SHORTCUTS - 1); + hasCachedShortcut = true; } } addOrUpdateConversationInfo(shortcut); } } + // Added at least one new conversation. Uncache older existing cached + // shortcuts to ensure the cache size is under the limit. + if (hasCachedShortcut) { + cleanupCachedShortcuts(user.getIdentifier(), + MAX_CACHED_RECENT_SHORTCUTS); + } }); } diff --git a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java index 35a677e0f816..817b245a78bf 100644 --- a/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/display/PersistentDataStoreTest.java @@ -377,6 +377,33 @@ public class PersistentDataStoreTest { assertTrue(Float.isNaN(mDataStore.getBrightness(testDisplayDevice))); } + @Test + public void testStoreAndRestoreBrightnessNitsForDefaultDisplay() { + float brightnessNitsForDefaultDisplay = 190; + mDataStore.loadIfNeeded(); + mDataStore.setBrightnessNitsForDefaultDisplay(brightnessNitsForDefaultDisplay); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + mInjector.setWriteStream(baos); + mDataStore.saveIfNeeded(); + mTestLooper.dispatchAll(); + assertTrue(mInjector.wasWriteSuccessful()); + TestInjector newInjector = new TestInjector(); + PersistentDataStore newDataStore = new PersistentDataStore(newInjector); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + newInjector.setReadStream(bais); + newDataStore.loadIfNeeded(); + assertEquals(brightnessNitsForDefaultDisplay, + mDataStore.getBrightnessNitsForDefaultDisplay(), 0); + assertEquals(brightnessNitsForDefaultDisplay, + newDataStore.getBrightnessNitsForDefaultDisplay(), 0); + } + + @Test + public void testInitialBrightnessNitsForDefaultDisplay() { + mDataStore.loadIfNeeded(); + assertEquals(-1, mDataStore.getBrightnessNitsForDefaultDisplay(), 0); + } public class TestInjector extends PersistentDataStore.Injector { private InputStream mReadStream; diff --git a/services/tests/servicestests/src/com/android/server/media/projection/OWNERS b/services/tests/servicestests/src/com/android/server/media/projection/OWNERS new file mode 100644 index 000000000000..832bcd9d70e6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/media/projection/OWNERS @@ -0,0 +1 @@ +include /media/java/android/media/projection/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java index 299f15344dfa..16a02b678511 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java @@ -91,8 +91,16 @@ public final class ContactsQueryHelperTest { } @Test - public void testQueryException_returnsFalse() { - contentProvider.setThrowException(true); + public void testQuerySQLiteException_returnsFalse() { + contentProvider.setThrowSQLiteException(true); + + Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY); + assertFalse(mHelper.query(contactUri.toString())); + } + + @Test + public void testQueryIllegalArgumentException_returnsFalse() { + contentProvider.setThrowIllegalArgumentException(true); Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY); assertFalse(mHelper.query(contactUri.toString())); @@ -178,14 +186,18 @@ public final class ContactsQueryHelperTest { private class ContactsContentProvider extends MockContentProvider { private Map<Uri, Cursor> mUriPrefixToCursorMap = new ArrayMap<>(); - private boolean throwException = false; + private boolean mThrowSQLiteException = false; + private boolean mThrowIllegalArgumentException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - if (throwException) { + if (mThrowSQLiteException) { throw new SQLiteException(); } + if (mThrowIllegalArgumentException) { + throw new IllegalArgumentException(); + } for (Uri prefixUri : mUriPrefixToCursorMap.keySet()) { if (uri.isPathPrefixMatch(prefixUri)) { @@ -195,8 +207,12 @@ public final class ContactsQueryHelperTest { return mUriPrefixToCursorMap.get(uri); } - public void setThrowException(boolean throwException) { - this.throwException = throwException; + public void setThrowSQLiteException(boolean throwException) { + this.mThrowSQLiteException = throwException; + } + + public void setThrowIllegalArgumentException(boolean throwException) { + this.mThrowIllegalArgumentException = throwException; } private void registerCursor(Uri uriPrefix, Cursor cursor) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 96e2a0916bb1..99a361c03a2a 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -18,6 +18,8 @@ package com.android.server.notification; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; +import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_FOREGROUND_SERVICE; +import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.SHOW_IMMEDIATELY; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.Notification.FLAG_AUTO_CANCEL; import static android.app.Notification.FLAG_BUBBLE; @@ -1160,6 +1162,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testEnqueuedBlockedNotifications_appBlockedChannelForegroundService() throws Exception { when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false); + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY); NotificationChannel channel = new NotificationChannel("blocked", "name", NotificationManager.IMPORTANCE_NONE); @@ -1182,6 +1186,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testEnqueuedBlockedNotifications_userBlockedChannelForegroundService() throws Exception { when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false); + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY); NotificationChannel channel = new NotificationChannel("blockedbyuser", "name", IMPORTANCE_HIGH); @@ -1261,6 +1267,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testEnqueuedBlockedNotifications_blockedAppForegroundService() throws Exception { when(mPackageManager.isPackageSuspendedForUser(anyString(), anyInt())).thenReturn(false); when(mPermissionHelper.hasPermission(mUid)).thenReturn(false); + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY); final StatusBarNotification sbn = generateNotificationRecord(null).getSbn(); sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; @@ -1590,6 +1598,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testEnqueueNotificationWithTag_FgsAddsFlags_dismissalAllowed() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY); + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); DeviceConfig.setProperty( DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED, @@ -1618,6 +1630,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testEnqueueNotificationWithTag_FGSaddsFlags_dismissalNotAllowed() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY); + mContext.getTestablePermissions().setPermission( + android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); DeviceConfig.setProperty( DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED, @@ -1904,6 +1920,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelAllNotifications_IgnoreForegroundService() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY); final StatusBarNotification sbn = generateNotificationRecord(null).getSbn(); sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; mBinderService.enqueueNotificationWithTag(PKG, PKG, @@ -1918,7 +1936,27 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testCancelAllNotifications_FgsFlag_NoFgs_Allowed() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(NOT_FOREGROUND_SERVICE); + final StatusBarNotification sbn = generateNotificationRecord(null).getSbn(); + sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; + mBinderService.enqueueNotificationWithTag(PKG, PKG, + "testCancelAllNotifications_IgnoreForegroundService", + sbn.getId(), sbn.getNotification(), sbn.getUserId()); + mBinderService.cancelAllNotifications(PKG, sbn.getUserId()); + waitForIdle(); + StatusBarNotification[] notifs = + mBinderService.getActiveNotifications(sbn.getPackageName()); + assertEquals(0, notifs.length); + } + + @Test public void testCancelAllNotifications_IgnoreOtherPackages() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); final StatusBarNotification sbn = generateNotificationRecord(null).getSbn(); sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; mBinderService.enqueueNotificationWithTag(PKG, PKG, @@ -2006,6 +2044,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testRemoveForegroundServiceFlag_ImmediatelyAfterEnqueue() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -2043,6 +2084,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationWithTag_fromApp_cannotCancelFgsChild() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); mService.isSystemUid = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); @@ -2066,6 +2110,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationWithTag_fromApp_cannotCancelFgsParent() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); mService.isSystemUid = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); @@ -2135,6 +2182,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelAllNotificationsFromApp_cannotCancelFgsChild() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); mService.isSystemUid = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); @@ -2160,6 +2210,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelAllNotifications_fromApp_cannotCancelFgsParent() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); mService.isSystemUid = false; final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); @@ -2281,6 +2334,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationsFromListener_clearAll_GroupWithFgsParent() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); parent.getNotification().flags |= FLAG_FOREGROUND_SERVICE; @@ -2304,6 +2360,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationsFromListener_clearAll_GroupWithFgsChild() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -2402,6 +2461,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationsFromListener_clearAll_Fgs() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); final NotificationRecord child2 = generateNotificationRecord( mTestNotificationChannel, 3, null, false); child2.getNotification().flags |= FLAG_FOREGROUND_SERVICE; @@ -2466,6 +2528,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationsFromListener_byKey_GroupWithFgsParent() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); parent.getNotification().flags |= FLAG_FOREGROUND_SERVICE; @@ -2491,6 +2556,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationsFromListener_byKey_GroupWithFgsChild() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -2596,6 +2664,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationsFromListener_byKey_Fgs() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); final NotificationRecord child2 = generateNotificationRecord( mTestNotificationChannel, 3, null, false); child2.getNotification().flags |= FLAG_FOREGROUND_SERVICE; @@ -2762,6 +2833,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUserInitiatedCancelAllWithGroup_ForegroundServiceFlag() throws Exception { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); final NotificationRecord child = generateNotificationRecord( @@ -6199,6 +6273,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testRemoveForegroundServiceFlagFromNotification_enqueued() { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); Notification n = new Notification.Builder(mContext, "").build(); n.flags |= FLAG_FOREGROUND_SERVICE; @@ -6218,6 +6295,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testRemoveForegroundServiceFlagFromNotification_posted() { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); Notification n = new Notification.Builder(mContext, "").build(); n.flags |= FLAG_FOREGROUND_SERVICE; @@ -6240,6 +6320,68 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testCannotRemoveForegroundFlagWhenOverLimit_enqueued() { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); + for (int i = 0; i < NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS; i++) { + Notification n = new Notification.Builder(mContext, "").build(); + StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, i, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + mService.addEnqueuedNotification(r); + } + Notification n = new Notification.Builder(mContext, "").build(); + n.flags |= FLAG_FOREGROUND_SERVICE; + + StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, + NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + mService.addEnqueuedNotification(r); + + mInternalService.removeForegroundServiceFlagFromNotification( + PKG, r.getSbn().getId(), r.getSbn().getUserId()); + + waitForIdle(); + + assertEquals(NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS, + mService.getNotificationRecordCount()); + } + + @Test + public void testCannotRemoveForegroundFlagWhenOverLimit_posted() { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); + for (int i = 0; i < NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS; i++) { + Notification n = new Notification.Builder(mContext, "").build(); + StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, i, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + mService.addNotification(r); + } + Notification n = new Notification.Builder(mContext, "").build(); + n.flags |= FLAG_FOREGROUND_SERVICE; + + StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, + NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS, null, mUid, 0, + n, UserHandle.getUserHandleForUid(mUid), null, 0); + NotificationRecord r = new NotificationRecord(mContext, sbn, mTestNotificationChannel); + + mService.addNotification(r); + + mInternalService.removeForegroundServiceFlagFromNotification( + PKG, r.getSbn().getId(), r.getSbn().getUserId()); + + waitForIdle(); + + assertEquals(NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS, + mService.getNotificationRecordCount()); + } + + @Test public void testAllowForegroundCustomToasts() throws Exception { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); @@ -8230,7 +8372,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertNotNull(n.publicVersion.bigContentView); assertNotNull(n.publicVersion.headsUpContentView); - mService.fixNotification(n, PKG, "tag", 9, 0); + mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE); assertNull(n.contentView); assertNull(n.bigContentView); @@ -8921,6 +9063,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCanPostFgsWhenOverLimit() throws RemoteException { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); for (int i = 0; i < NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS; i++) { StatusBarNotification sbn = generateNotificationRecord(mTestNotificationChannel, i, null, false).getSbn(); @@ -8946,6 +9091,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCannotPostNonFgsWhenOverLimit() throws RemoteException { + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(SHOW_IMMEDIATELY); for (int i = 0; i < NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS; i++) { StatusBarNotification sbn = generateNotificationRecord(mTestNotificationChannel, i, null, false).getSbn(); @@ -8968,6 +9116,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { "testCanPostFgsWhenOverLimit - non fgs over limit!", sbn2.getId(), sbn2.getNotification(), sbn2.getUserId()); + + when(mAmi.applyForegroundServiceNotification( + any(), anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(NOT_FOREGROUND_SERVICE); + final StatusBarNotification sbn3 = generateNotificationRecord(mTestNotificationChannel, + 101, null, false).getSbn(); + sbn3.getNotification().flags |= FLAG_FOREGROUND_SERVICE; + mBinderService.enqueueNotificationWithTag(PKG, PKG, + "testCanPostFgsWhenOverLimit - fake fgs over limit!", + sbn3.getId(), sbn3.getNotification(), sbn3.getUserId()); + waitForIdle(); StatusBarNotification[] notifs = @@ -10025,4 +10184,21 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mInternalService.sendReviewPermissionsNotification(); verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class)); } + + @Test + public void fixNotification_withFgsFlag_butIsNotFgs() throws Exception { + final ApplicationInfo applicationInfo = new ApplicationInfo(); + when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + + Notification n = new Notification.Builder(mContext, "test") + .setFlag(FLAG_FOREGROUND_SERVICE, true) + .setFlag(FLAG_CAN_COLORIZE, true) + .build(); + + mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE); + + assertFalse(n.isForegroundService()); + assertFalse(n.hasColorizedPermission()); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index ea50179746d8..0300eb06c220 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2377,7 +2377,7 @@ public class ActivityRecordTests extends WindowTestsBase { .setScreenOrientation(SCREEN_ORIENTATION_BEHIND) .build(); final int topOrientation = activityTop.getRequestedConfigurationOrientation(); - assertEquals(SCREEN_ORIENTATION_PORTRAIT, topOrientation); + assertEquals(ORIENTATION_PORTRAIT, topOrientation); } private void verifyProcessInfoUpdate(ActivityRecord activity, State state, 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 117805bf344d..d77b6ada268e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -381,7 +381,7 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - public void testTranslucentActivitiesDontGoInSizeCompactMode() { + public void testTranslucentActivitiesDontGoInSizeCompatMode() { mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true); setUpDisplaySizeWithApp(2800, 1400); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); @@ -2454,11 +2454,11 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(mActivity.inSizeCompatMode()); mActivity.setRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED); - - assertTrue(mActivity.inSizeCompatMode()); - // We should remember the original orientation. + // Activity is not in size compat mode because the orientation change request came from the + // app itself + assertFalse(mActivity.inSizeCompatMode()); assertEquals(mActivity.getResolvedOverrideConfiguration().orientation, - Configuration.ORIENTATION_PORTRAIT); + Configuration.ORIENTATION_UNDEFINED); } @Test @@ -3033,6 +3033,25 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testAppRequestsOrientationChange_notInSizeCompat() { + setUpDisplaySizeWithApp(2200, 1800); + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + + prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); + + mActivity.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); + + // Activity is not in size compat mode because the orientation change request came from the + // app itself + assertFalse(mActivity.inSizeCompatMode()); + + rotateDisplay(mActivity.mDisplayContent, ROTATION_270); + // Activity should go into size compat mode now because the orientation change came from the + // system (device rotation) + assertTrue(mActivity.inSizeCompatMode()); + } + + @Test public void testLetterboxDetailsForStatusBar_noLetterbox() { setUpDisplaySizeWithApp(2800, 1000); addStatusBar(mActivity.mDisplayContent); diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index 1407cdd8600c..65f31a0e15bb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -52,6 +52,7 @@ import android.graphics.Rect; import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.util.MergedConfiguration; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.Gravity; @@ -61,6 +62,7 @@ import android.view.RoundedCorners; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; +import android.window.ClientWindowFrames; import androidx.test.filters.SmallTest; @@ -338,6 +340,29 @@ public class WallpaperControllerTests extends WindowTestsBase { } @Test + public void testWallpaperReportConfigChange() { + final WindowState wallpaperWindow = createWallpaperWindow(mDisplayContent); + createWallpaperTargetWindow(mDisplayContent); + final WallpaperWindowToken wallpaperToken = wallpaperWindow.mToken.asWallpaperToken(); + makeWindowVisible(wallpaperWindow); + wallpaperWindow.mLayoutSeq = mDisplayContent.mLayoutSeq; + // Assume the token was invisible and the latest config was reported. + wallpaperToken.commitVisibility(false); + wallpaperWindow.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(), + new MergedConfiguration(), true /* useLatestConfig */, false /* relayoutVisible */); + assertTrue(wallpaperWindow.isLastConfigReportedToClient()); + + final Rect bounds = wallpaperToken.getBounds(); + wallpaperToken.setBounds(new Rect(0, 0, bounds.width() / 2, bounds.height() / 2)); + assertFalse(wallpaperWindow.isLastConfigReportedToClient()); + // If there is a pending config change when changing to visible, it should tell the client + // to redraw by WindowState#reportResized. + wallpaperToken.commitVisibility(true); + waitUntilHandlersIdle(); + assertTrue(wallpaperWindow.isLastConfigReportedToClient()); + } + + @Test public void testWallpaperTokenVisibility() { final DisplayContent dc = mWm.mRoot.getDefaultDisplay(); final WindowState wallpaperWindow = createWallpaperWindow(dc); |