diff options
189 files changed, 6179 insertions, 2066 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/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/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/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/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 7d3e7ca671e5..2f0cb7e55c98 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 @@ -764,17 +764,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); if (pendingIntent2 == null) { - // Launching a solo task. - ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); - options1 = activityOptions.toBundle(); - addActivityOptions(options1, null /* launchTarget */); - if (shortcutInfo1 != null) { - wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1); - } else { - wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1); - } - mSyncQueue.queue(wct); + // Launching a solo intent or shortcut as fullscreen. + launchAsFullscreenWithRemoteAnimation(pendingIntent1, fillInIntent1, shortcutInfo1, + options1, adapter, wct); return; } @@ -797,13 +789,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); if (taskId == INVALID_TASK_ID) { - // Launching a solo task. - ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); - options1 = activityOptions.toBundle(); - addActivityOptions(options1, null /* launchTarget */); - wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - mSyncQueue.queue(wct); + // Launching a solo intent as fullscreen. + launchAsFullscreenWithRemoteAnimation(pendingIntent, fillInIntent, null, options1, + adapter, wct); return; } @@ -822,13 +810,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); if (taskId == INVALID_TASK_ID) { - // Launching a solo task. - ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); - activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); - options1 = activityOptions.toBundle(); - addActivityOptions(options1, null /* launchTarget */); - wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); - mSyncQueue.queue(wct); + // Launching a solo shortcut as fullscreen. + launchAsFullscreenWithRemoteAnimation(null, null, shortcutInfo, options1, adapter, wct); return; } @@ -838,6 +821,49 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, instanceId); } + private void launchAsFullscreenWithRemoteAnimation(@Nullable PendingIntent pendingIntent, + @Nullable Intent fillInIntent, @Nullable ShortcutInfo shortcutInfo, + @Nullable Bundle options, RemoteAnimationAdapter adapter, + WindowContainerTransaction wct) { + LegacyTransitions.ILegacyTransition transition = + (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { + if (apps == null || apps.length == 0) { + onRemoteAnimationFinished(apps); + t.apply(); + try { + adapter.getRunner().onAnimationCancelled(mKeyguardShowing); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + return; + } + + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } + t.apply(); + + try { + adapter.getRunner().onAnimationStart( + transit, apps, wallpapers, nonApps, finishedCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + }; + + addActivityOptions(options, null /* launchTarget */); + if (shortcutInfo != null) { + wct.startShortcut(mContext.getPackageName(), shortcutInfo, options); + } else if (pendingIntent != null) { + wct.sendPendingIntent(pendingIntent, fillInIntent, options); + } else { + Slog.e(TAG, "Pending intent and shortcut are null is invalid case."); + } + mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); + } + private void startWithLegacyTransition(WindowContainerTransaction wct, @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle mainOptions, @@ -894,23 +920,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (options == null) options = new Bundle(); addActivityOptions(options, mMainStage); - options = wrapAsSplitRemoteAnimation(adapter, options); updateWindowBounds(mSplitLayout, wct); + wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); // TODO(b/268008375): Merge APIs to start a split pair into one. if (mainTaskId != INVALID_TASK_ID) { + options = wrapAsSplitRemoteAnimation(adapter, options); wct.startTask(mainTaskId, options); - } else if (mainShortcutInfo != null) { - wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options); + mSyncQueue.queue(wct); } else { - wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options); + if (mainShortcutInfo != null) { + wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options); + } else { + wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options); + } + mSyncQueue.queue(wrapAsSplitRemoteAnimation(adapter), WindowManager.TRANSIT_OPEN, wct); } - wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); - - mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { setDividerVisibility(true, t); }); @@ -967,6 +995,54 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return activityOptions.toBundle(); } + private LegacyTransitions.ILegacyTransition wrapAsSplitRemoteAnimation( + RemoteAnimationAdapter adapter) { + LegacyTransitions.ILegacyTransition transition = + (transit, apps, wallpapers, nonApps, finishedCallback, t) -> { + if (apps == null || apps.length == 0) { + onRemoteAnimationFinished(apps); + t.apply(); + try { + adapter.getRunner().onAnimationCancelled(mKeyguardShowing); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + return; + } + + // Wrap the divider bar into non-apps target to animate together. + nonApps = ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps, + getDividerBarLegacyTarget()); + + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + // Reset the surface position of the opening app to prevent offset. + t.setPosition(apps[i].leash, 0, 0); + } + } + t.apply(); + + IRemoteAnimationFinishedCallback wrapCallback = + new IRemoteAnimationFinishedCallback.Stub() { + @Override + public void onAnimationFinished() throws RemoteException { + onRemoteAnimationFinished(apps); + finishedCallback.onAnimationFinished(); + } + }; + Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication()); + try { + adapter.getRunner().onAnimationStart( + transit, apps, wallpapers, nonApps, wrapCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + }; + + return transition; + } + private void setEnterInstanceId(InstanceId instanceId) { if (instanceId != null) { mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER); @@ -993,6 +1069,27 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } + private void onRemoteAnimationFinished(RemoteAnimationTarget[] apps) { + mIsDividerRemoteAnimating = false; + mShouldUpdateRecents = true; + mSplitRequest = null; + // If any stage has no child after finished animation, that side of the split will display + // nothing. This might happen if starting the same app on the both sides while not + // supporting multi-instance. Exit the split screen and expand that app to full screen. + if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0 + ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); + mSplitUnsupportedToast.show(); + return; + } + + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictNonOpeningChildTasks(SPLIT_POSITION_TOP_OR_LEFT, apps, evictWct); + prepareEvictNonOpeningChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, apps, evictWct); + mSyncQueue.queue(evictWct); + } + + /** * Collects all the current child tasks of a specific split and prepares transaction to evict * them to display. @@ -1687,7 +1784,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Split entering background. wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, true /* setReparentLeafTaskIfRelaunch */); - wct.setForceTranslucent(mRootTaskInfo.token, true); + if (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping) { + wct.setForceTranslucent(mRootTaskInfo.token, true); + } } else { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* setReparentLeafTaskIfRelaunch */); 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/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java index 5ecec4ddd1ad..3125f088c72b 100644 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java @@ -72,6 +72,7 @@ public final class LowLightDreamManager { public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2; private final DreamManager mDreamManager; + private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; @Nullable private final ComponentName mLowLightDreamComponent; @@ -81,8 +82,10 @@ public final class LowLightDreamManager { @Inject public LowLightDreamManager( DreamManager dreamManager, + LowLightTransitionCoordinator lowLightTransitionCoordinator, @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) { mDreamManager = dreamManager; + mLowLightTransitionCoordinator = lowLightTransitionCoordinator; mLowLightDreamComponent = lowLightDreamComponent; } @@ -111,7 +114,9 @@ public final class LowLightDreamManager { mAmbientLightMode = ambientLightMode; - mDreamManager.setSystemDreamComponent(mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT - ? mLowLightDreamComponent : null); + boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT; + mLowLightTransitionCoordinator.notifyBeforeLowLightTransition(shouldEnterLowLight, + () -> mDreamManager.setSystemDreamComponent( + shouldEnterLowLight ? mLowLightDreamComponent : null)); } } diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java new file mode 100644 index 000000000000..874a2d5af75e --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dream.lowlight; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.Nullable; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Helper class that allows listening and running animations before entering or exiting low light. + */ +@Singleton +public class LowLightTransitionCoordinator { + /** + * Listener that is notified before low light entry. + */ + public interface LowLightEnterListener { + /** + * Callback that is notified before the device enters low light. + * + * @return an optional animator that will be waited upon before entering low light. + */ + Animator onBeforeEnterLowLight(); + } + + /** + * Listener that is notified before low light exit. + */ + public interface LowLightExitListener { + /** + * Callback that is notified before the device exits low light. + * + * @return an optional animator that will be waited upon before exiting low light. + */ + Animator onBeforeExitLowLight(); + } + + private LowLightEnterListener mLowLightEnterListener; + private LowLightExitListener mLowLightExitListener; + + @Inject + public LowLightTransitionCoordinator() { + } + + /** + * Sets the listener for the low light enter event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) { + mLowLightEnterListener = lowLightEnterListener; + } + + /** + * Sets the listener for the low light exit event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) { + mLowLightExitListener = lowLightExitListener; + } + + /** + * Notifies listeners that the device is about to enter or exit low light. + * + * @param entering true if listeners should be notified before entering low light, false if this + * is notifying before exiting. + * @param callback callback that will be run after listeners complete. + */ + void notifyBeforeLowLightTransition(boolean entering, Runnable callback) { + Animator animator = null; + + if (entering && mLowLightEnterListener != null) { + animator = mLowLightEnterListener.onBeforeEnterLowLight(); + } else if (!entering && mLowLightExitListener != null) { + animator = mLowLightExitListener.onBeforeExitLowLight(); + } + + // If the listener returned an animator to indicate it was running an animation, run the + // callback after the animation completes, otherwise call the callback directly. + if (animator != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animator) { + callback.run(); + } + }); + } else { + callback.run(); + } + } +} diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java index 91a170f7ae14..4b95d8c84bac 100644 --- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java @@ -21,7 +21,10 @@ import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -44,44 +47,52 @@ public class LowLightDreamManagerTest { private DreamManager mDreamManager; @Mock + private LowLightTransitionCoordinator mTransitionCoordinator; + + @Mock private ComponentName mDreamComponent; + LowLightDreamManager mLowLightDreamManager; + @Before public void setUp() { MockitoAnnotations.initMocks(this); + + // Automatically run any provided Runnable to mTransitionCoordinator to simplify testing. + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(1)).run(); + return null; + }).when(mTransitionCoordinator).notifyBeforeLowLightTransition(anyBoolean(), + any(Runnable.class)); + + mLowLightDreamManager = new LowLightDreamManager(mDreamManager, mTransitionCoordinator, + mDreamComponent); } @Test public void setAmbientLightMode_lowLight_setSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(true), any()); verify(mDreamManager).setSystemDreamComponent(mDreamComponent); } @Test public void setAmbientLightMode_regularLight_clearSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); + verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(false), any()); verify(mDreamManager).setSystemDreamComponent(null); } @Test public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mDreamComponent); - // Set to low light first. - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); clearInvocations(mDreamManager); // Return to default unknown mode. - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); + mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); verify(mDreamManager).setSystemDreamComponent(null); } @@ -89,7 +100,7 @@ public class LowLightDreamManagerTest { @Test public void setAmbientLightMode_dreamComponentNotSet_doNothing() { final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - null /*dream component*/); + mTransitionCoordinator, null /*dream component*/); lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java new file mode 100644 index 000000000000..81e1e33d6220 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java @@ -0,0 +1,113 @@ +/* + * 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.dream.lowlight; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.animation.Animator; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class LowLightTransitionCoordinatorTest { + @Mock + private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener; + + @Mock + private LowLightTransitionCoordinator.LowLightExitListener mExitListener; + + @Mock + private Animator mAnimator; + + @Captor + private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor; + + @Mock + private Runnable mRunnable; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void onEnterCalledOnListeners() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightEnterListener(mEnterListener); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + verify(mEnterListener).onBeforeEnterLowLight(); + verify(mRunnable).run(); + } + + @Test + public void onExitCalledOnListeners() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightExitListener(mExitListener); + + coordinator.notifyBeforeLowLightTransition(false, mRunnable); + + verify(mExitListener).onBeforeExitLowLight(); + verify(mRunnable).run(); + } + + @Test + public void listenerNotCalledAfterRemoval() { + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + + coordinator.setLowLightEnterListener(mEnterListener); + coordinator.setLowLightEnterListener(null); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + verifyZeroInteractions(mEnterListener); + verify(mRunnable).run(); + } + + @Test + public void runnableCalledAfterAnimationEnds() { + when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator); + + LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); + coordinator.setLowLightEnterListener(mEnterListener); + + coordinator.notifyBeforeLowLightTransition(true, mRunnable); + + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); + verifyZeroInteractions(mRunnable); + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.getValue().onAnimationEnd(null); + verify(mRunnable).run(); + } +} 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/config.xml b/packages/SystemUI/res/values/config.xml index 3f84ddb2a067..f545dae05b56 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -774,7 +774,7 @@ <!-- Duration in milliseconds of the dream in complications fade-in animation. --> <integer name="config_dreamOverlayInComplicationsDurationMs">250</integer> <!-- Duration in milliseconds of the y-translation animation when entering a dream --> - <integer name="config_dreamOverlayInTranslationYDurationMs">917</integer> + <integer name="config_dreamOverlayInTranslationYDurationMs">1167</integer> <!-- Delay in milliseconds before switching to the dock user and dreaming if a secondary user is active when the device is locked and docked. 0 indicates disabled. Default is 1 minute. --> 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/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt index fe8b8c944d13..c98e9b40e7ab 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt @@ -40,7 +40,7 @@ data class KeyguardFaceListenModel( var keyguardGoingAway: Boolean = false, var listeningForFaceAssistant: Boolean = false, var occludingAppRequestingFaceAuth: Boolean = false, - val postureAllowsListening: Boolean = false, + var postureAllowsListening: Boolean = false, var primaryUser: Boolean = false, var secureCameraLaunched: Boolean = false, var supportsDetect: Boolean = false, @@ -70,6 +70,7 @@ data class KeyguardFaceListenModel( listeningForFaceAssistant.toString(), occludingAppRequestingFaceAuth.toString(), primaryUser.toString(), + postureAllowsListening.toString(), secureCameraLaunched.toString(), supportsDetect.toString(), switchingUser.toString(), @@ -109,6 +110,7 @@ data class KeyguardFaceListenModel( listeningForFaceAssistant = model.listeningForFaceAssistant occludingAppRequestingFaceAuth = model.occludingAppRequestingFaceAuth primaryUser = model.primaryUser + postureAllowsListening = model.postureAllowsListening secureCameraLaunched = model.secureCameraLaunched supportsDetect = model.supportsDetect switchingUser = model.switchingUser @@ -152,6 +154,7 @@ data class KeyguardFaceListenModel( "listeningForFaceAssistant", "occludingAppRequestingFaceAuth", "primaryUser", + "postureAllowsListening", "secureCameraLaunched", "supportsDetect", "switchingUser", 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 b27cca41dae2..b2f4c30211e7 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -153,6 +153,7 @@ import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.DevicePostureController; +import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.Assert; import com.android.systemui.util.settings.SecureSettings; @@ -359,7 +360,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final FaceManager mFaceManager; private final LockPatternUtils mLockPatternUtils; @VisibleForTesting - @DevicePostureController.DevicePostureInt + @DevicePostureInt protected int mConfigFaceAuthSupportedPosture; private KeyguardBypassController mKeyguardBypassController; @@ -707,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); } @@ -1846,10 +1853,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final DevicePostureController.Callback mPostureCallback = new DevicePostureController.Callback() { @Override - public void onPostureChanged(int posture) { + public void onPostureChanged(@DevicePostureInt int posture) { + boolean currentPostureAllowsFaceAuth = doesPostureAllowFaceAuth(mPostureState); + boolean newPostureAllowsFaceAuth = doesPostureAllowFaceAuth(posture); mPostureState = posture; - updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_UPDATED_POSTURE_CHANGED); + if (currentPostureAllowsFaceAuth && !newPostureAllowsFaceAuth) { + mLogger.d("New posture does not allow face auth, stopping it"); + updateFaceListeningState(BIOMETRIC_ACTION_STOP, + FACE_AUTH_UPDATED_POSTURE_CHANGED); + } } }; @@ -2886,9 +2898,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user); final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant(); final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown(); - final boolean isPostureAllowedForFaceAuth = - mConfigFaceAuthSupportedPosture == 0 /* DEVICE_POSTURE_UNKNOWN */ ? true - : (mPostureState == mConfigFaceAuthSupportedPosture); + final boolean isPostureAllowedForFaceAuth = doesPostureAllowFaceAuth(mPostureState); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. final boolean shouldListen = @@ -2937,6 +2947,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return shouldListen; } + private boolean doesPostureAllowFaceAuth(@DevicePostureInt int posture) { + return mConfigFaceAuthSupportedPosture == DEVICE_POSTURE_UNKNOWN + || (posture == mConfigFaceAuthSupportedPosture); + } + private void logListenerModelData(@NonNull KeyguardListenModel model) { mLogger.logKeyguardListenerModel(model); if (model instanceof KeyguardFingerprintListenModel) { @@ -3635,7 +3650,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/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 3555d0a7e7fb..2d37c292a6b8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -173,12 +173,6 @@ interface ControlsController : UserAwareController { fun removeFavorites(componentName: ComponentName): Boolean /** - * Checks if the favorites can be removed. You can't remove components from the preferred list. - * @param componentName the name of the service that provides the [Control] - */ - fun canRemoveFavorites(componentName: ComponentName): Boolean - - /** * Replaces the favorites for the given structure. * * Calling this method will eliminate the previous selection of favorites and replace it with a diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 854790360f6a..e8c97bf77271 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -37,6 +37,7 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.SysUISingleton @@ -55,16 +56,17 @@ import javax.inject.Inject @SysUISingleton class ControlsControllerImpl @Inject constructor ( - private val context: Context, - @Background private val executor: DelayableExecutor, - private val uiController: ControlsUiController, - private val bindingController: ControlsBindingController, - private val listingController: ControlsListingController, - private val userFileManager: UserFileManager, - private val userTracker: UserTracker, - private val authorizedPanelsRepository: AuthorizedPanelsRepository, - optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, - dumpManager: DumpManager, + private val context: Context, + @Background private val executor: DelayableExecutor, + private val uiController: ControlsUiController, + private val selectedComponentRepository: SelectedComponentRepository, + private val bindingController: ControlsBindingController, + private val listingController: ControlsListingController, + private val userFileManager: UserFileManager, + private val userTracker: UserTracker, + private val authorizedPanelsRepository: AuthorizedPanelsRepository, + optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>, + dumpManager: DumpManager, ) : Dumpable, ControlsController { companion object { @@ -497,17 +499,14 @@ class ControlsControllerImpl @Inject constructor ( } } - override fun canRemoveFavorites(componentName: ComponentName): Boolean = - !authorizedPanelsRepository.getPreferredPackages().contains(componentName.packageName) - override fun removeFavorites(componentName: ComponentName): Boolean { if (!confirmAvailability()) return false - if (!canRemoveFavorites(componentName)) return false executor.execute { - Favorites.removeStructures(componentName) + if (Favorites.removeStructures(componentName)) { + persistenceWrapper.storeFavorites(Favorites.getAllStructures()) + } authorizedPanelsRepository.removeAuthorizedPanels(setOf(componentName.packageName)) - persistenceWrapper.storeFavorites(Favorites.getAllStructures()) } return true } @@ -574,7 +573,9 @@ class ControlsControllerImpl @Inject constructor ( } override fun setPreferredSelection(selectedItem: SelectedItem) { - uiController.updatePreferences(selectedItem) + selectedComponentRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(selectedItem) + ) } override fun dump(pw: PrintWriter, args: Array<out String>) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index d949d1119222..2af49aa5fa1a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -36,6 +36,8 @@ import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.management.ControlsRequestDialog import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.panels.AuthorizedPanelsRepositoryImpl +import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.SelectedComponentRepositoryImpl import com.android.systemui.controls.settings.ControlsSettingsDialogManager import com.android.systemui.controls.settings.ControlsSettingsDialogManagerImpl import com.android.systemui.controls.ui.ControlActionCoordinator @@ -114,6 +116,11 @@ abstract class ControlsModule { repository: AuthorizedPanelsRepositoryImpl ): AuthorizedPanelsRepository + @Binds + abstract fun providePreferredPanelRepository( + repository: SelectedComponentRepositoryImpl + ): SelectedComponentRepository + @BindsOptionalOf abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt index e51e8326c0a5..5c2402ba4149 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImpl.kt @@ -20,6 +20,8 @@ package com.android.systemui.controls.panels import android.content.Context import android.content.SharedPreferences import com.android.systemui.R +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl @@ -30,7 +32,8 @@ class AuthorizedPanelsRepositoryImpl constructor( private val context: Context, private val userFileManager: UserFileManager, - private val userTracker: UserTracker + private val userTracker: UserTracker, + private val featureFlags: FeatureFlags, ) : AuthorizedPanelsRepository { override fun getAuthorizedPanels(): Set<String> { @@ -71,8 +74,18 @@ constructor( userTracker.userId, ) - // If we've never run this (i.e., the key doesn't exist), add the default packages - if (sharedPref.getStringSet(KEY, null) == null) { + // We should add default packages in two cases: + // 1) We've never run this + // 2) APP_PANELS_REMOVE_APPS_ALLOWED got disabled after user removed all apps + val needToSetup = + if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED)) { + sharedPref.getStringSet(KEY, null) == null + } else { + // There might be an empty set that need to be overridden after the feature has been + // turned off after being turned on + sharedPref.getStringSet(KEY, null).isNullOrEmpty() + } + if (needToSetup) { sharedPref.edit().putStringSet(KEY, getPreferredPackages()).apply() } return sharedPref diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt new file mode 100644 index 000000000000..5bb6eece9098 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepository.kt @@ -0,0 +1,64 @@ +/* + * 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.controls.panels + +import android.content.ComponentName +import com.android.systemui.controls.ui.ControlsUiController +import com.android.systemui.controls.ui.SelectedItem +import com.android.systemui.flags.Flags + +/** Stores user-selected preferred component. */ +interface SelectedComponentRepository { + + /** + * Returns currently set preferred component, or null when nothing is set. Consider using + * [ControlsUiController.getPreferredSelectedItem] to get domain specific data + */ + fun getSelectedComponent(): SelectedComponent? + + /** Sets preferred component. Use [getSelectedComponent] to get current one */ + fun setSelectedComponent(selectedComponent: SelectedComponent) + + /** Clears current preferred component. [getSelectedComponent] will return null afterwards */ + fun removeSelectedComponent() + + /** + * Return true when default preferred component should be set up and false the otherwise. This + * is always true when [Flags.APP_PANELS_REMOVE_APPS_ALLOWED] is disabled + */ + fun shouldAddDefaultComponent(): Boolean + + /** + * Sets if default component should be added. This is ignored when + * [Flags.APP_PANELS_REMOVE_APPS_ALLOWED] is disabled + */ + fun setShouldAddDefaultComponent(shouldAdd: Boolean) + + data class SelectedComponent( + val name: String, + val componentName: ComponentName?, + val isPanel: Boolean, + ) { + constructor( + selectedItem: SelectedItem + ) : this( + name = selectedItem.name.toString(), + componentName = selectedItem.componentName, + isPanel = selectedItem is SelectedItem.PanelItem, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt new file mode 100644 index 000000000000..0fb5b66ef93c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/panels/SelectedComponentRepositoryImpl.kt @@ -0,0 +1,95 @@ +/* + * 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.controls.panels + +import android.content.ComponentName +import android.content.Context +import android.content.SharedPreferences +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import javax.inject.Inject + +@SysUISingleton +class SelectedComponentRepositoryImpl +@Inject +constructor( + private val userFileManager: UserFileManager, + private val userTracker: UserTracker, + private val featureFlags: FeatureFlags, +) : SelectedComponentRepository { + + private companion object { + const val PREF_COMPONENT = "controls_component" + const val PREF_STRUCTURE_OR_APP_NAME = "controls_structure" + const val PREF_IS_PANEL = "controls_is_panel" + const val SHOULD_ADD_DEFAULT_PANEL = "should_add_default_panel" + } + + private val sharedPreferences: SharedPreferences + get() = + userFileManager.getSharedPreferences( + fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + mode = Context.MODE_PRIVATE, + userId = userTracker.userId + ) + + override fun getSelectedComponent(): SelectedComponentRepository.SelectedComponent? { + with(sharedPreferences) { + val componentString = getString(PREF_COMPONENT, null) ?: return null + return SelectedComponentRepository.SelectedComponent( + name = getString(PREF_STRUCTURE_OR_APP_NAME, "")!!, + componentName = ComponentName.unflattenFromString(componentString), + isPanel = getBoolean(PREF_IS_PANEL, false) + ) + } + } + + override fun setSelectedComponent( + selectedComponent: SelectedComponentRepository.SelectedComponent + ) { + sharedPreferences + .edit() + .putString(PREF_COMPONENT, selectedComponent.componentName?.flattenToString()) + .putString(PREF_STRUCTURE_OR_APP_NAME, selectedComponent.name) + .putBoolean(PREF_IS_PANEL, selectedComponent.isPanel) + .apply() + } + + override fun removeSelectedComponent() { + sharedPreferences + .edit() + .remove(PREF_COMPONENT) + .remove(PREF_STRUCTURE_OR_APP_NAME) + .remove(PREF_IS_PANEL) + .apply() + } + + override fun shouldAddDefaultComponent(): Boolean = + if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED)) { + sharedPreferences.getBoolean(SHOULD_ADD_DEFAULT_PANEL, true) + } else { + true + } + + override fun setShouldAddDefaultComponent(shouldAdd: Boolean) { + sharedPreferences.edit().putBoolean(SHOULD_ADD_DEFAULT_PANEL, shouldAdd).apply() + } +} 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 9d99253de741..3a4a00c0ccd3 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/start/ControlsStartable.kt @@ -18,17 +18,16 @@ package com.android.systemui.controls.start import android.content.Context -import android.content.res.Resources import android.os.UserHandle import com.android.systemui.CoreStartable -import com.android.systemui.R import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.UserTracker import java.util.concurrent.Executor import javax.inject.Inject @@ -37,7 +36,7 @@ import javax.inject.Inject * Started with SystemUI to perform early operations for device controls subsystem (only if enabled) * * In particular, it will perform the following: - * * If there is no preferred selection for provider and at least one of the preferred packages + * * If there is no preferred selection for provider and at least one of the preferred packages * provides a panel, it will select the first one that does. * * If the preferred selection provides a panel, it will bind to that service (to reduce latency on * displaying the panel). @@ -48,10 +47,11 @@ import javax.inject.Inject class ControlsStartable @Inject constructor( - @Main private val resources: Resources, - @Background private val executor: Executor, - private val controlsComponent: ControlsComponent, - private val userTracker: UserTracker + @Background private val executor: Executor, + private val controlsComponent: ControlsComponent, + private val userTracker: UserTracker, + private val authorizedPanelsRepository: AuthorizedPanelsRepository, + private val selectedComponentRepository: SelectedComponentRepository, ) : CoreStartable { // These two controllers can only be accessed after `start` method once we've checked if the @@ -85,12 +85,15 @@ constructor( } private fun selectDefaultPanelIfNecessary() { + if (!selectedComponentRepository.shouldAddDefaultComponent()) { + return + } val currentSelection = controlsController.getPreferredSelection() if (currentSelection == SelectedItem.EMPTY_SELECTION) { val availableServices = controlsListingController.getCurrentServices() val panels = availableServices.filter { it.panelActivity != null } - resources - .getStringArray(R.array.config_controlsPreferredPackages) + authorizedPanelsRepository + .getPreferredPackages() // Looking for the first element in the string array such that there is one package // that has a panel. It will return null if there are no packages in the array, // or if no packages in the array have a panel associated with it. diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 58673bb6f567..0d5311752ab9 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -64,8 +64,6 @@ interface ControlsUiController { * This element will be the one that appears when the user first opens the controls activity. */ fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem - - fun updatePreferences(selectedItem: SelectedItem) } sealed class SelectedItem { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index c61dad6fc075..5da86de933e6 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -64,6 +64,7 @@ import com.android.systemui.controls.management.ControlsFavoritingActivity import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.settings.ControlsSettingsRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -73,9 +74,7 @@ import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.globalactions.GlobalActionsPopupMenu import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.asIndenting import com.android.systemui.util.concurrency.DelayableExecutor @@ -84,7 +83,7 @@ import com.android.wm.shell.TaskViewFactory import dagger.Lazy import java.io.PrintWriter import java.text.Collator -import java.util.* +import java.util.Optional import java.util.function.Consumer import javax.inject.Inject @@ -98,25 +97,22 @@ class ControlsUiControllerImpl @Inject constructor ( @Main val uiExecutor: DelayableExecutor, @Background val bgExecutor: DelayableExecutor, val controlsListingController: Lazy<ControlsListingController>, - val controlActionCoordinator: ControlActionCoordinator, + private val controlActionCoordinator: ControlActionCoordinator, private val activityStarter: ActivityStarter, private val iconCache: CustomIconCache, private val controlsMetricsLogger: ControlsMetricsLogger, private val keyguardStateController: KeyguardStateController, - private val userFileManager: UserFileManager, private val userTracker: UserTracker, private val taskViewFactory: Optional<TaskViewFactory>, private val controlsSettingsRepository: ControlsSettingsRepository, private val authorizedPanelsRepository: AuthorizedPanelsRepository, + private val selectedComponentRepository: SelectedComponentRepository, private val featureFlags: FeatureFlags, private val dialogsFactory: ControlsDialogsFactory, dumpManager: DumpManager ) : ControlsUiController, Dumpable { companion object { - private const val PREF_COMPONENT = "controls_component" - private const val PREF_STRUCTURE_OR_APP_NAME = "controls_structure" - private const val PREF_IS_PANEL = "controls_is_panel" private const val FADE_IN_MILLIS = 200L @@ -138,12 +134,6 @@ class ControlsUiControllerImpl @Inject constructor ( private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) private var retainCache = false private var lastSelections = emptyList<SelectionItem>() - private val sharedPreferences - get() = userFileManager.getSharedPreferences( - fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - mode = 0, - userId = userTracker.userId - ) private var taskViewController: PanelTaskViewController? = null @@ -341,20 +331,18 @@ class ControlsUiControllerImpl @Inject constructor ( if (!controlsController.get().removeFavorites(componentName)) { return@createRemoveAppDialog } - if ( - sharedPreferences.getString(PREF_COMPONENT, "") == - componentName.flattenToString() - ) { - sharedPreferences - .edit() - .remove(PREF_COMPONENT) - .remove(PREF_STRUCTURE_OR_APP_NAME) - .remove(PREF_IS_PANEL) - .commit() + + if (selectedComponentRepository.getSelectedComponent()?.componentName == + componentName) { + selectedComponentRepository.removeSelectedComponent() } - allStructures = controlsController.get().getFavorites() - selectedItem = getPreferredSelectedItem(allStructures) + val selectedItem = getPreferredSelectedItem(controlsController.get().getFavorites()) + if (selectedItem == SelectedItem.EMPTY_SELECTION) { + // User removed the last panel. In this case we start app selection flow and don't + // want to auto-add it again + selectedComponentRepository.setShouldAddDefaultComponent(false) + } reload(parent) }.apply { show() } } @@ -522,8 +510,7 @@ class ControlsUiControllerImpl @Inject constructor ( ADD_APP_ID )) } - if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED) && - controlsController.get().canRemoveFavorites(selectedItem.componentName)) { + if (featureFlags.isEnabled(Flags.APP_PANELS_REMOVE_APPS_ALLOWED)) { add(OverflowMenuAdapter.MenuItem( context.getText(R.string.controls_menu_remove), REMOVE_APP_ID, @@ -569,7 +556,7 @@ class ControlsUiControllerImpl @Inject constructor ( ADD_CONTROLS_ID -> startFavoritingActivity(selectedStructure) EDIT_CONTROLS_ID -> startEditingActivity(selectedStructure) REMOVE_APP_ID -> startRemovingApp( - selectedStructure.componentName, selectionItem.appName + selectionItem.componentName, selectionItem.appName ) } dismiss() @@ -714,29 +701,22 @@ class ControlsUiControllerImpl @Inject constructor ( } override fun getPreferredSelectedItem(structures: List<StructureInfo>): SelectedItem { - val sp = sharedPreferences - - val component = sp.getString(PREF_COMPONENT, null)?.let { - ComponentName.unflattenFromString(it) - } ?: EMPTY_COMPONENT - val name = sp.getString(PREF_STRUCTURE_OR_APP_NAME, "")!! - val isPanel = sp.getBoolean(PREF_IS_PANEL, false) - return if (isPanel) { - SelectedItem.PanelItem(name, component) + val preferredPanel = selectedComponentRepository.getSelectedComponent() + val component = preferredPanel?.componentName ?: EMPTY_COMPONENT + return if (preferredPanel?.isPanel == true) { + SelectedItem.PanelItem(preferredPanel.name, component) } else { if (structures.isEmpty()) return SelectedItem.EMPTY_SELECTION SelectedItem.StructureItem(structures.firstOrNull { - component == it.componentName && name == it.structure - } ?: structures.get(0)) + component == it.componentName && preferredPanel?.name == it.structure + } ?: structures[0]) } } - override fun updatePreferences(selectedItem: SelectedItem) { - sharedPreferences.edit() - .putString(PREF_COMPONENT, selectedItem.componentName.flattenToString()) - .putString(PREF_STRUCTURE_OR_APP_NAME, selectedItem.name.toString()) - .putBoolean(PREF_IS_PANEL, selectedItem is SelectedItem.PanelItem) - .apply() + private fun updatePreferences(selectedItem: SelectedItem) { + selectedComponentRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(selectedItem) + ) } private fun maybeUpdateSelectedItem(item: SelectionItem): Boolean { 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/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index ca1cef385755..d0a92f0846d0 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -43,7 +43,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch @@ -131,9 +130,17 @@ constructor( } } - /** Starts the dream content and dream overlay entry animations. */ + /** + * Starts the dream content and dream overlay entry animations. + * + * @param downwards if true, the entry animation translations downwards into position rather + * than upwards. + */ @JvmOverloads - fun startEntryAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }) { + fun startEntryAnimations( + downwards: Boolean, + animatorBuilder: () -> AnimatorSet = { AnimatorSet() } + ) { cancelAnimations() mAnimator = @@ -153,7 +160,7 @@ constructor( interpolator = Interpolators.LINEAR ), translationYAnimator( - from = mDreamInTranslationYDistance.toFloat(), + from = mDreamInTranslationYDistance.toFloat() * (if (downwards) -1 else 1), to = 0f, durationMs = mDreamInTranslationYDurationMs, interpolator = Interpolators.EMPHASIZED_DECELERATE @@ -167,6 +174,71 @@ constructor( } } + /** + * Starts the dream content and dream overlay exit animations. + * + * This should only be used when the low light dream is entering, animations to/from other SysUI + * views is controlled by `transitionViewModel`. + */ + // TODO(b/256916668): integrate with the keyguard transition model once dream surfaces work is + // done. + @JvmOverloads + fun startExitAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }): Animator { + cancelAnimations() + + mAnimator = + animatorBuilder().apply { + playTogether( + translationYAnimator( + from = 0f, + to = -mDreamInTranslationYDistance.toFloat(), + durationMs = mDreamInTranslationYDurationMs, + delayMs = 0, + interpolator = Interpolators.EMPHASIZED + ), + alphaAnimator( + from = + mCurrentAlphaAtPosition.getOrDefault( + key = POSITION_BOTTOM, + defaultValue = 1f + ), + to = 0f, + durationMs = mDreamInComplicationsAnimDurationMs, + delayMs = 0, + positions = POSITION_BOTTOM + ) + .apply { + doOnEnd { + // The logical end of the animation is once the alpha and blur + // animations finish, end the animation so that any listeners are + // notified. The Y translation animation is much longer than all of + // the other animations due to how the spec is defined, but is not + // expected to run to completion. + mAnimator?.end() + } + }, + alphaAnimator( + from = + mCurrentAlphaAtPosition.getOrDefault( + key = POSITION_TOP, + defaultValue = 1f + ), + to = 0f, + durationMs = mDreamInComplicationsAnimDurationMs, + delayMs = 0, + positions = POSITION_TOP + ) + ) + doOnEnd { + mAnimator = null + mOverlayStateController.setExitAnimationsRunning(false) + } + start() + } + mOverlayStateController.setExitAnimationsRunning(true) + return mAnimator as AnimatorSet + } + /** Starts the dream content and dream overlay exit animations. */ fun wakeUp(doneCallback: Runnable, executor: DelayableExecutor) { cancelAnimations() @@ -182,19 +254,6 @@ constructor( } } - /** - * Ends the dream content and dream overlay animations, if they're currently running. - * - * @see [AnimatorSet.end] - */ - fun endAnimations() { - mAnimator = - mAnimator?.let { - it.end() - null - } - } - private fun blurAnimator( view: View, fromBlurRadius: Float, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 50cfb6a905c9..4b478cdca9f9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -23,6 +23,7 @@ import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_BOTTOM; import static com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP; +import android.animation.Animator; import android.content.res.Resources; import android.os.Handler; import android.util.MathUtils; @@ -31,6 +32,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; +import com.android.dream.lowlight.LowLightTransitionCoordinator; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.dagger.qualifiers.Main; @@ -54,11 +56,14 @@ import javax.inject.Named; * View controller for {@link DreamOverlayContainerView}. */ @DreamOverlayComponent.DreamOverlayScope -public class DreamOverlayContainerViewController extends ViewController<DreamOverlayContainerView> { +public class DreamOverlayContainerViewController extends + ViewController<DreamOverlayContainerView> implements + LowLightTransitionCoordinator.LowLightEnterListener { private final DreamOverlayStatusBarViewController mStatusBarViewController; private final BlurUtils mBlurUtils; private final DreamOverlayAnimationsController mDreamOverlayAnimationsController; private final DreamOverlayStateController mStateController; + private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; private final ComplicationHostViewController mComplicationHostViewController; @@ -143,19 +148,18 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve }; /** - * If true, overlay entry animations should be skipped once. - * - * This is turned on when exiting low light and should be turned off once the entry animations - * are skipped once. + * If {@code true}, the dream has just transitioned from the low light dream back to the user + * dream and we should play an entry animation where the overlay slides in downwards from the + * top instead of the typicla slide in upwards from the bottom. */ - private boolean mSkipEntryAnimations; + private boolean mExitingLowLight; private final DreamOverlayStateController.Callback mDreamOverlayStateCallback = new DreamOverlayStateController.Callback() { @Override public void onExitLowLight() { - mSkipEntryAnimations = true; + mExitingLowLight = true; } }; @@ -165,6 +169,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve ComplicationHostViewController complicationHostViewController, @Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView, DreamOverlayStatusBarViewController statusBarViewController, + LowLightTransitionCoordinator lowLightTransitionCoordinator, BlurUtils blurUtils, @Main Handler handler, @Main Resources resources, @@ -182,6 +187,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mBlurUtils = blurUtils; mDreamOverlayAnimationsController = animationsController; mStateController = stateController; + mLowLightTransitionCoordinator = lowLightTransitionCoordinator; mBouncerlessScrimController = bouncerlessScrimController; mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback); @@ -208,6 +214,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mStatusBarViewController.init(); mComplicationHostViewController.init(); mDreamOverlayAnimationsController.init(mView); + mLowLightTransitionCoordinator.setLowLightEnterListener(this); } @Override @@ -219,14 +226,10 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve // Start dream entry animations. Skip animations for low light clock. if (!mStateController.isLowLightActive()) { - mDreamOverlayAnimationsController.startEntryAnimations(); - - if (mSkipEntryAnimations) { - // If we're transitioning from the low light dream back to the user dream, skip the - // overlay animations and show immediately. - mDreamOverlayAnimationsController.endAnimations(); - mSkipEntryAnimations = false; - } + // If this is transitioning from the low light dream to the user dream, the overlay + // should translate in downwards instead of upwards. + mDreamOverlayAnimationsController.startEntryAnimations(mExitingLowLight); + mExitingLowLight = false; } } @@ -310,4 +313,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve mDreamOverlayAnimationsController.wakeUp(onAnimationEnd, callbackExecutor); } + + @Override + public Animator onBeforeEnterLowLight() { + // Return the animator so that the transition coordinator waits for the overlay exit + // animations to finish before entering low light, as otherwise the default DreamActivity + // animation plays immediately and there's no time for this animation to play. + return mDreamOverlayAnimationsController.startExitAnimations(); + } } 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..03c98d28a626 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 = @@ -388,13 +391,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 +488,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 +603,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 +675,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") } 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/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/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 3be24173c21b..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 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/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/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/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/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 3c80dad2bd82..4c92eddd66fb 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -17,7 +17,6 @@ package com.android.keyguard; import static android.app.StatusBarManager.SESSION_KEYGUARD; -import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_LOCKOUT_TIMED; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON; @@ -2200,6 +2199,32 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testPostureChangeToUnsupported_stopsFaceListeningState() { + // GIVEN device is listening for face + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_CLOSED; + deviceInPostureStateClosed(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); + mTestableLooper.processAllMessages(); + keyguardIsVisible(); + + verifyFaceAuthenticateCall(); + + final CancellationSignal faceCancel = spy(mKeyguardUpdateMonitor.mFaceCancelSignal); + mKeyguardUpdateMonitor.mFaceCancelSignal = faceCancel; + KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class); + mKeyguardUpdateMonitor.registerCallback(callback); + + // WHEN device is opened + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + + // THEN face listening is stopped. + verify(faceCancel).cancel(); + verify(callback).onBiometricRunningStateChanged( + eq(false), eq(BiometricSourceType.FACE)); + } + + @Test public void testShouldListenForFace_withLockedDown_returnsFalse() throws RemoteException { keyguardNotGoingAway(); 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/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index 28e80057a672..c98d5374311d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.controls.ControlStatus import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.FakeSelectedComponentRepository import com.android.systemui.controls.ui.ControlsUiController import com.android.systemui.dump.DumpManager import com.android.systemui.settings.UserFileManager @@ -56,7 +57,6 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.inOrder @@ -66,6 +66,7 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import java.io.File import java.util.* @@ -108,6 +109,8 @@ class ControlsControllerImplTest : SysuiTestCase() { private lateinit var listingCallbackCaptor: ArgumentCaptor<ControlsListingController.ControlsListingCallback> + private val preferredPanelRepository = FakeSelectedComponentRepository() + private lateinit var delayableExecutor: FakeExecutor private lateinit var controller: ControlsControllerImpl private lateinit var canceller: DidRunRunnable @@ -168,6 +171,7 @@ class ControlsControllerImplTest : SysuiTestCase() { wrapper, delayableExecutor, uiController, + preferredPanelRepository, bindingController, listingController, userFileManager, @@ -221,6 +225,7 @@ class ControlsControllerImplTest : SysuiTestCase() { mContext, delayableExecutor, uiController, + preferredPanelRepository, bindingController, listingController, userFileManager, @@ -240,6 +245,7 @@ class ControlsControllerImplTest : SysuiTestCase() { mContext, delayableExecutor, uiController, + preferredPanelRepository, bindingController, listingController, userFileManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt index 7ac1953ee495..272f5895390c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/AuthorizedPanelsRepositoryImplTest.kt @@ -22,6 +22,8 @@ import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.util.FakeSharedPreferences @@ -40,6 +42,8 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker + private val featureFlags = FakeFeatureFlags() + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -48,6 +52,7 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { arrayOf<String>() ) whenever(userTracker.userId).thenReturn(0) + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, true) } @Test @@ -127,8 +132,25 @@ class AuthorizedPanelsRepositoryImplTest : SysuiTestCase() { assertThat(sharedPrefs.getStringSet(KEY, null)).isEmpty() } + @Test + fun testSetAuthorizedPackageAfterFeatureDisabled() { + mContext.orCreateTestableResources.addOverride( + R.array.config_controlsPreferredPackages, + arrayOf(TEST_PACKAGE) + ) + val sharedPrefs = FakeSharedPreferences() + val fileManager = FakeUserFileManager(mapOf(0 to sharedPrefs)) + val repository = createRepository(fileManager) + + repository.removeAuthorizedPanels(setOf(TEST_PACKAGE)) + + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, false) + + assertThat(repository.getAuthorizedPanels()).isEqualTo(setOf(TEST_PACKAGE)) + } + private fun createRepository(userFileManager: UserFileManager): AuthorizedPanelsRepositoryImpl { - return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker) + return AuthorizedPanelsRepositoryImpl(mContext, userFileManager, userTracker, featureFlags) } private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) : diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt new file mode 100644 index 000000000000..a7677cca9f29 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt @@ -0,0 +1,42 @@ +/* + * 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.controls.panels + +class FakeSelectedComponentRepository : SelectedComponentRepository { + + private var selectedComponent: SelectedComponentRepository.SelectedComponent? = null + private var shouldAddDefaultPanel: Boolean = true + + override fun getSelectedComponent(): SelectedComponentRepository.SelectedComponent? = + selectedComponent + + override fun setSelectedComponent( + selectedComponent: SelectedComponentRepository.SelectedComponent + ) { + this.selectedComponent = selectedComponent + } + + override fun removeSelectedComponent() { + selectedComponent = null + } + + override fun shouldAddDefaultComponent(): Boolean = shouldAddDefaultPanel + + override fun setShouldAddDefaultComponent(shouldAdd: Boolean) { + shouldAddDefaultPanel = shouldAdd + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt new file mode 100644 index 000000000000..0c7b9cb82b94 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/panels/SelectedComponentRepositoryTest.kt @@ -0,0 +1,163 @@ +/* + * 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.controls.panels + +import android.content.ComponentName +import android.content.SharedPreferences +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl +import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class SelectedComponentRepositoryTest : SysuiTestCase() { + + private companion object { + val COMPONENT_A = + SelectedComponentRepository.SelectedComponent( + name = "a", + componentName = ComponentName.unflattenFromString("pkg/.cls_a"), + isPanel = false, + ) + val COMPONENT_B = + SelectedComponentRepository.SelectedComponent( + name = "b", + componentName = ComponentName.unflattenFromString("pkg/.cls_b"), + isPanel = false, + ) + } + + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var userFileManager: UserFileManager + + private val featureFlags = FakeFeatureFlags() + private val sharedPreferences: SharedPreferences = FakeSharedPreferences() + + // under test + private lateinit var repository: SelectedComponentRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(userFileManager.getSharedPreferences(any(), any(), any())) + .thenReturn(sharedPreferences) + + repository = SelectedComponentRepositoryImpl(userFileManager, userTracker, featureFlags) + } + + @Test + fun testUnsetIsNull() { + assertThat(repository.getSelectedComponent()).isNull() + } + + @Test + fun testGetReturnsSet() { + repository.setSelectedComponent(COMPONENT_A) + + assertThat(repository.getSelectedComponent()).isEqualTo(COMPONENT_A) + } + + @Test + fun testSetOverrides() { + repository.setSelectedComponent(COMPONENT_A) + repository.setSelectedComponent(COMPONENT_B) + + assertThat(repository.getSelectedComponent()).isEqualTo(COMPONENT_B) + } + + @Test + fun testRemove() { + repository.setSelectedComponent(COMPONENT_A) + + repository.removeSelectedComponent() + + assertThat(repository.getSelectedComponent()).isNull() + } + + @Test + fun testFeatureEnabled_shouldAddDefaultPanelDefaultsToTrue() { + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, true) + + assertThat(repository.shouldAddDefaultComponent()).isTrue() + } + + @Test + fun testFeatureDisabled_shouldAddDefaultPanelDefaultsToTrue() { + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, false) + + assertThat(repository.shouldAddDefaultComponent()).isTrue() + } + + @Test + fun testFeatureEnabled_shouldAddDefaultPanelChecked() { + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, true) + repository.setShouldAddDefaultComponent(false) + + assertThat(repository.shouldAddDefaultComponent()).isFalse() + } + + @Test + fun testFeatureDisabled_shouldAlwaysAddDefaultPanelAlwaysTrue() { + featureFlags.set(Flags.APP_PANELS_REMOVE_APPS_ALLOWED, false) + repository.setShouldAddDefaultComponent(false) + + assertThat(repository.shouldAddDefaultComponent()).isTrue() + } + + @Test + fun testGetPreferredStructure_differentUserId() { + sharedPreferences.savePanel(COMPONENT_A) + whenever( + userFileManager.getSharedPreferences( + DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, + 0, + 1, + ) + ) + .thenReturn(FakeSharedPreferences().also { it.savePanel(COMPONENT_B) }) + + val previousPreferredStructure = repository.getSelectedComponent() + whenever(userTracker.userId).thenReturn(1) + val currentPreferredStructure = repository.getSelectedComponent() + + assertThat(previousPreferredStructure).isEqualTo(COMPONENT_A) + assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure) + assertThat(currentPreferredStructure).isEqualTo(COMPONENT_B) + } + + private fun SharedPreferences.savePanel(panel: SelectedComponentRepository.SelectedComponent) { + edit() + .putString("controls_component", panel.componentName?.flattenToString()) + .putString("controls_structure", panel.name) + .putBoolean("controls_is_panel", panel.isPanel) + .commit() + } +} 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 7ecaca6c36d0..9d8084d4f2f4 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 @@ -23,17 +23,19 @@ import android.content.pm.ApplicationInfo import android.content.pm.ServiceInfo import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.FakeSelectedComponentRepository import com.android.systemui.controls.ui.SelectedItem import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import java.util.Optional import org.junit.Before @@ -53,16 +55,16 @@ class ControlsStartableTest : SysuiTestCase() { @Mock private lateinit var controlsController: ControlsController @Mock private lateinit var controlsListingController: ControlsListingController @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository + + private val preferredPanelsRepository = FakeSelectedComponentRepository() private lateinit var fakeExecutor: FakeExecutor @Before fun setUp() { MockitoAnnotations.initMocks(this) - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf<String>() - ) + whenever(authorizedPanelsRepository.getPreferredPackages()).thenReturn(setOf()) fakeExecutor = FakeExecutor(FakeSystemClock()) } @@ -87,10 +89,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testPreferredPackagesNotInstalled_noNewSelection() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) `when`(controlsListingController.getCurrentServices()).thenReturn(emptyList()) @@ -101,10 +101,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testPreferredPackageNotPanel_noNewSelection() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .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) @@ -116,10 +114,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testExistingSelection_noNewSelection() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()) .thenReturn(mock<SelectedItem.PanelItem>()) val listings = listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) @@ -132,10 +128,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testPanelAdded() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .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) @@ -147,10 +141,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testMultiplePreferredOnlyOnePanel_panelAdded() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf("other_package", TEST_PACKAGE_PANEL) - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf( @@ -166,10 +158,8 @@ class ControlsStartableTest : SysuiTestCase() { @Test fun testMultiplePreferredMultiplePanels_firstPreferredAdded() { - context.orCreateTestableResources.addOverride( - R.array.config_controlsPreferredPackages, - arrayOf(TEST_PACKAGE_PANEL, "other_package") - ) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) `when`(controlsController.getPreferredSelection()).thenReturn(SelectedItem.EMPTY_SELECTION) val listings = listOf( @@ -217,6 +207,20 @@ class ControlsStartableTest : SysuiTestCase() { verify(controlsController, never()).bindComponentForPanel(any()) } + @Test + fun testAlreadyAddedPanel_noNewSelection() { + preferredPanelsRepository.setShouldAddDefaultComponent(false) + whenever(authorizedPanelsRepository.getPreferredPackages()) + .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) + + createStartable(enabled = true).start() + + verify(controlsController, never()).setPreferredSelection(any()) + } + private fun createStartable(enabled: Boolean): ControlsStartable { val component: ControlsComponent = mock() { @@ -230,7 +234,13 @@ class ControlsStartableTest : SysuiTestCase() { `when`(getControlsListingController()).thenReturn(Optional.empty()) } } - return ControlsStartable(context.resources, fakeExecutor, component, userTracker) + return ControlsStartable( + fakeExecutor, + component, + userTracker, + authorizedPanelsRepository, + preferredPanelsRepository, + ) } private fun ControlsServiceInfo( diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 23faa99c0b9d..3f61bf75740a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -42,16 +42,14 @@ import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.FakeSelectedComponentRepository +import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.shade.ShadeController -import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.FakeSystemUIDialogController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -64,20 +62,18 @@ import com.android.systemui.util.time.FakeSystemClock import com.android.wm.shell.TaskView import com.android.wm.shell.TaskViewFactory import com.google.common.truth.Truth.assertThat +import java.util.Optional +import java.util.function.Consumer 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.anyInt -import org.mockito.Mockito.anyString import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.util.Optional -import java.util.function.Consumer @SmallTest @RunWith(AndroidTestingRunner::class) @@ -87,11 +83,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var controlsListingController: ControlsListingController @Mock lateinit var controlActionCoordinator: ControlActionCoordinator @Mock lateinit var activityStarter: ActivityStarter - @Mock lateinit var shadeController: ShadeController @Mock lateinit var iconCache: CustomIconCache @Mock lateinit var controlsMetricsLogger: ControlsMetricsLogger @Mock lateinit var keyguardStateController: KeyguardStateController - @Mock lateinit var userFileManager: UserFileManager @Mock lateinit var userTracker: UserTracker @Mock lateinit var taskViewFactory: TaskViewFactory @Mock lateinit var dumpManager: DumpManager @@ -99,7 +93,7 @@ class ControlsUiControllerImplTest : SysuiTestCase() { @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var packageManager: PackageManager - private val sharedPreferences = FakeSharedPreferences() + private val preferredPanelRepository = FakeSelectedComponentRepository() private val fakeDialogController = FakeSystemUIDialogController() private val uiExecutor = FakeExecutor(FakeSystemClock()) private val bgExecutor = FakeExecutor(FakeSystemClock()) @@ -138,94 +132,30 @@ class ControlsUiControllerImplTest : SysuiTestCase() { iconCache, controlsMetricsLogger, keyguardStateController, - userFileManager, userTracker, Optional.of(taskViewFactory), controlsSettingsRepository, authorizedPanelsRepository, + preferredPanelRepository, featureFlags, ControlsDialogsFactory { fakeDialogController.dialog }, dumpManager, ) - `when`( - userFileManager.getSharedPreferences( - DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - 0, - 0 - ) - ) - .thenReturn(sharedPreferences) - `when`(userFileManager.getSharedPreferences(anyString(), anyInt(), anyInt())) - .thenReturn(sharedPreferences) `when`(userTracker.userId).thenReturn(0) `when`(userTracker.userHandle).thenReturn(UserHandle.of(0)) } @Test - fun testGetPreferredStructure() { - val structureInfo = mock<StructureInfo>() - underTest.getPreferredSelectedItem(listOf(structureInfo)) - verify(userFileManager) - .getSharedPreferences( - fileName = DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - mode = 0, - userId = 0 - ) - } - - @Test - fun testGetPreferredStructure_differentUserId() { - val selectedItems = - listOf( - SelectedItem.StructureItem( - StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()) - ), - SelectedItem.StructureItem( - StructureInfo(ComponentName.unflattenFromString("pkg/.cls2"), "b", ArrayList()) - ), - ) - val structures = selectedItems.map { it.structure } - sharedPreferences - .edit() - .putString("controls_component", selectedItems[0].componentName.flattenToString()) - .putString("controls_structure", selectedItems[0].name.toString()) - .commit() - - val differentSharedPreferences = FakeSharedPreferences() - differentSharedPreferences - .edit() - .putString("controls_component", selectedItems[1].componentName.flattenToString()) - .putString("controls_structure", selectedItems[1].name.toString()) - .commit() - - val previousPreferredStructure = underTest.getPreferredSelectedItem(structures) - - `when`( - userFileManager.getSharedPreferences( - DeviceControlsControllerImpl.PREFS_CONTROLS_FILE, - 0, - 1 - ) - ) - .thenReturn(differentSharedPreferences) - `when`(userTracker.userId).thenReturn(1) - - val currentPreferredStructure = underTest.getPreferredSelectedItem(structures) - - assertThat(previousPreferredStructure).isEqualTo(selectedItems[0]) - assertThat(currentPreferredStructure).isEqualTo(selectedItems[1]) - assertThat(currentPreferredStructure).isNotEqualTo(previousPreferredStructure) - } - - @Test fun testGetPreferredPanel() { val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) - sharedPreferences - .edit() - .putString("controls_component", panel.componentName.flattenToString()) - .putString("controls_structure", panel.appName.toString()) - .putBoolean("controls_is_panel", true) - .commit() + + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent( + name = panel.appName.toString(), + componentName = panel.componentName, + isPanel = true, + ) + ) val selected = underTest.getPreferredSelectedItem(emptyList()) @@ -369,11 +299,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()) ), ) - sharedPreferences - .edit() - .putString("controls_component", selectedItems[0].componentName.flattenToString()) - .putString("controls_structure", selectedItems[0].name.toString()) - .commit() + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(selectedItems[0]) + ) assertThat(underTest.resolveActivity()) .isEqualTo(ControlsProviderSelectorActivity::class.java) @@ -418,12 +346,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { val componentName = ComponentName(context, "cls") whenever(controlsController.removeFavorites(eq(componentName))).thenReturn(true) val panel = SelectedItem.PanelItem("App name", componentName) - sharedPreferences - .edit() - .putString("controls_component", panel.componentName.flattenToString()) - .putString("controls_structure", panel.appName.toString()) - .putBoolean("controls_is_panel", true) - .commit() + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(panel) + ) underTest.show(parent, {}, context) underTest.startRemovingApp(componentName, "Test App") @@ -432,11 +357,8 @@ class ControlsUiControllerImplTest : SysuiTestCase() { verify(controlsController).removeFavorites(eq(componentName)) assertThat(underTest.getPreferredSelectedItem(emptyList())) .isEqualTo(SelectedItem.EMPTY_SELECTION) - with(sharedPreferences) { - assertThat(contains("controls_component")).isFalse() - assertThat(contains("controls_structure")).isFalse() - assertThat(contains("controls_is_panel")).isFalse() - } + assertThat(preferredPanelRepository.shouldAddDefaultComponent()).isFalse() + assertThat(preferredPanelRepository.getSelectedComponent()).isNull() } @Test @@ -452,12 +374,9 @@ class ControlsUiControllerImplTest : SysuiTestCase() { private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo { val activity = ComponentName(context, "activity") - sharedPreferences - .edit() - .putString("controls_component", panel.componentName.flattenToString()) - .putString("controls_structure", panel.appName.toString()) - .putBoolean("controls_is_panel", true) - .commit() + preferredPanelRepository.setSelectedComponent( + SelectedComponentRepository.SelectedComponent(panel) + ) return ControlsServiceInfo(panel.componentName, panel.appName, activity) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 6c23254941a8..0a9470617a5f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -1,6 +1,8 @@ package com.android.systemui.dreams +import android.animation.Animator import android.animation.AnimatorSet +import android.animation.ValueAnimator import android.testing.AndroidTestingRunner import android.view.View import androidx.test.filters.SmallTest @@ -10,13 +12,16 @@ import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransition import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito.anyLong import org.mockito.Mockito.eq @@ -71,6 +76,19 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { } @Test + fun testExitAnimationUpdatesState() { + controller.startExitAnimations(animatorBuilder = { mockAnimator }) + + verify(stateController).setExitAnimationsRunning(true) + + val captor = argumentCaptor<Animator.AnimatorListener>() + verify(mockAnimator).addListener(captor.capture()) + + captor.value.onAnimationEnd(mockAnimator) + verify(stateController).setExitAnimationsRunning(false) + } + + @Test fun testWakeUpCallsExecutor() { val mockExecutor: DelayableExecutor = mock() val mockCallback: Runnable = mock() @@ -87,7 +105,7 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { fun testWakeUpAfterStartWillCancel() { val mockStartAnimator: AnimatorSet = mock() - controller.startEntryAnimations(animatorBuilder = { mockStartAnimator }) + controller.startEntryAnimations(false, animatorBuilder = { mockStartAnimator }) verify(mockStartAnimator, never()).cancel() @@ -100,4 +118,50 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { // animator. verify(mockStartAnimator, times(1)).cancel() } + + @Test + fun testEntryAnimations_translatesUpwards() { + val mockStartAnimator: AnimatorSet = mock() + + controller.startEntryAnimations( + /* downwards= */ false, + animatorBuilder = { mockStartAnimator } + ) + + val animatorCaptor = ArgumentCaptor.forClass(Animator::class.java) + verify(mockStartAnimator).playTogether(animatorCaptor.capture()) + + // Check if there's a ValueAnimator starting at the expected Y distance. + val animators: List<ValueAnimator> = animatorCaptor.allValues as List<ValueAnimator> + assertTrue( + animators.any { + // Call setCurrentFraction so the animated value jumps to the initial value. + it.setCurrentFraction(0f) + it.animatedValue == DREAM_IN_TRANSLATION_Y_DISTANCE.toFloat() + } + ) + } + + @Test + fun testEntryAnimations_translatesDownwards() { + val mockStartAnimator: AnimatorSet = mock() + + controller.startEntryAnimations( + /* downwards= */ true, + animatorBuilder = { mockStartAnimator } + ) + + val animatorCaptor = ArgumentCaptor.forClass(Animator::class.java) + verify(mockStartAnimator).playTogether(animatorCaptor.capture()) + + // Check if there's a ValueAnimator starting at the expected Y distance. + val animators: List<ValueAnimator> = animatorCaptor.allValues as List<ValueAnimator> + assertTrue( + animators.any { + // Call setCurrentFraction so the animated value jumps to the initial value. + it.setCurrentFraction(0f) + it.animatedValue == -DREAM_IN_TRANSLATION_Y_DISTANCE.toFloat() + } + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index 6b095ffd3977..2a72e7d85d3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -17,6 +17,7 @@ package com.android.systemui.dreams; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -33,6 +34,7 @@ import android.view.ViewTreeObserver; import androidx.test.filters.SmallTest; +import com.android.dream.lowlight.LowLightTransitionCoordinator; import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.complication.ComplicationHostViewController; @@ -65,6 +67,9 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { DreamOverlayStatusBarViewController mDreamOverlayStatusBarViewController; @Mock + LowLightTransitionCoordinator mLowLightTransitionCoordinator; + + @Mock DreamOverlayContainerView mDreamOverlayContainerView; @Mock @@ -109,6 +114,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mComplicationHostViewController, mDreamOverlayContentView, mDreamOverlayStatusBarViewController, + mLowLightTransitionCoordinator, mBlurUtils, mHandler, mResources, @@ -200,7 +206,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); - verify(mAnimationsController).startEntryAnimations(); + verify(mAnimationsController).startEntryAnimations(false); verify(mAnimationsController, never()).cancelAnimations(); } @@ -210,11 +216,11 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); - verify(mAnimationsController, never()).startEntryAnimations(); + verify(mAnimationsController, never()).startEntryAnimations(anyBoolean()); } @Test - public void testSkipEntryAnimationsWhenExitingLowLight() { + public void testDownwardEntryAnimationsWhenExitingLowLight() { ArgumentCaptor<DreamOverlayStateController.Callback> callbackCaptor = ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); when(mStateController.isLowLightActive()).thenReturn(false); @@ -230,8 +236,14 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mController.onViewAttached(); // Entry animations should be started then immediately ended to skip to the end. - verify(mAnimationsController).startEntryAnimations(); - verify(mAnimationsController).endAnimations(); + verify(mAnimationsController).startEntryAnimations(true); + } + + @Test + public void testStartsExitAnimationsBeforeEnteringLowLight() { + mController.onBeforeEnterLowLight(); + + verify(mAnimationsController).startExitAnimations(); } @Test 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..fc3a6383cd88 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 @@ -967,6 +967,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/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/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/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/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 5ecc7f2b7bb8..c869fcdf989f 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -873,10 +873,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mAutomaticBrightnessController.stop(); } - if (mScreenOffBrightnessSensorController != null) { - mScreenOffBrightnessSensorController.stop(); - } - if (mBrightnessSetting != null) { mBrightnessSetting.unregisterListener(mBrightnessSettingListener); } @@ -1125,6 +1121,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mScreenOffBrightnessSensorController != null) { mScreenOffBrightnessSensorController.stop(); + mScreenOffBrightnessSensorController = null; } loadScreenOffBrightnessSensor(); int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux(); @@ -1242,6 +1239,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mPowerState.stop(); mPowerState = null; } + + if (mScreenOffBrightnessSensorController != null) { + mScreenOffBrightnessSensorController.stop(); + } } private void updatePowerState() { 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/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 c8f9db7f7e34..de5defa1bcd4 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(); + } } } @@ -8008,9 +8014,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // The smallest screen width is the short side of screen bounds. Because the bounds // and density won't be changed, smallestScreenWidthDp is also fixed. overrideConfig.smallestScreenWidthDp = fullConfig.smallestScreenWidthDp; - // TODO(b/264276741): Check whether the runtime orietnation request is fixed rather than - // the manifest orientation which may be obsolete. - if (info.isFixedOrientation()) { + if (ActivityInfo.isFixedOrientation(getOverrideOrientation())) { // lock rotation too. When in size-compat, onConfigurationChanged will watch for and // apply runtime rotation changes. overrideConfig.windowConfiguration.setRotation( @@ -8101,9 +8105,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. @@ -8120,7 +8124,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); @@ -8128,7 +8132,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 @@ -8175,7 +8179,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); @@ -8187,7 +8191,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** * @return The orientation to use to understand if reachability is enabled. */ - @ActivityInfo.ScreenOrientation + @Configuration.Orientation int getOrientationForReachability() { return mLetterboxUiController.hasInheritedLetterboxBehavior() ? mLetterboxUiController.getInheritedOrientation() @@ -8583,7 +8587,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(); @@ -8604,13 +8609,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 { @@ -8626,11 +8631,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); } @@ -8639,7 +8644,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/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 ce032442e4af..9e0e8c477573 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1436,7 +1436,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * {@link Configuration#ORIENTATION_PORTRAIT}, * {@link Configuration#ORIENTATION_UNDEFINED}). */ - @ScreenOrientation + @Configuration.Orientation int getRequestedConfigurationOrientation() { return getRequestedConfigurationOrientation(false /* forDisplay */); } @@ -1454,7 +1454,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * {@link Configuration#ORIENTATION_PORTRAIT}, * {@link Configuration#ORIENTATION_UNDEFINED}). */ - @ScreenOrientation + @Configuration.Orientation int getRequestedConfigurationOrientation(boolean forDisplay) { int requestedOrientation = getOverrideOrientation(); final RootDisplayArea root = getRootDisplayArea(); 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/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/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) { |